mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-25 14:02:53 -05:00
Compare commits
767 Commits
v0.5.0
...
45ba64ab02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45ba64ab02 | ||
|
|
9501d66af6 | ||
|
|
2127fc339a | ||
|
|
7962fee0bd | ||
|
|
d5c7b5c0cc | ||
|
|
5f77d69dd8 | ||
|
|
60034be06a | ||
|
|
518a5d38aa | ||
|
|
2eeaf8ff62 | ||
|
|
cffee0fae6 | ||
|
|
f08e2ef5b8 | ||
|
|
2b0070c31a | ||
|
|
ae82716afa | ||
|
|
c281bf3b53 | ||
|
|
45b8b2a895 | ||
|
|
7b9ba840fb | ||
|
|
84fb567ff5 | ||
|
|
bbd57e0673 | ||
|
|
b1632a0a03 | ||
|
|
7aff1182c8 | ||
|
|
fbe362cd20 | ||
|
|
e7f94c94cc | ||
|
|
7523190b16 | ||
|
|
da37e16b6e | ||
|
|
5c420ab50b | ||
|
|
4493b7c231 | ||
|
|
40a96c6eaf | ||
|
|
23a93082c6 | ||
|
|
f7650b5e1f | ||
|
|
3ebdd5631c | ||
|
|
6c4caf121a | ||
|
|
89788e9ca7 | ||
|
|
0787c63fed | ||
|
|
9fc0d5efff | ||
|
|
6611dfbe05 | ||
|
|
8a71ead51d | ||
|
|
d9d6ab5776 | ||
|
|
d6fe7bea27 | ||
|
|
1194f3ffb8 | ||
|
|
5ac81e6dd6 | ||
|
|
987856a1de | ||
|
|
ef52ce0990 | ||
|
|
06b14a5869 | ||
|
|
fd839059c0 | ||
|
|
ec6db7962a | ||
|
|
adf92cbc46 | ||
|
|
6b6f51cd1f | ||
|
|
df6c60213f | ||
|
|
6303304a10 | ||
|
|
8e76789119 | ||
|
|
10e81cfdd3 | ||
|
|
03fd3a4f16 | ||
|
|
8fdc748ed2 | ||
|
|
6c56d23b93 | ||
|
|
45d34dcb5b | ||
|
|
d7ac0d50fa | ||
|
|
1d4d145187 | ||
|
|
a5b9ff98c0 | ||
|
|
6feaecd92e | ||
|
|
b066a25308 | ||
|
|
777a552b57 | ||
|
|
7dbe608c28 | ||
|
|
61630e447b | ||
|
|
91385e7c83 | ||
|
|
04648fcca7 | ||
|
|
080fc7e44e | ||
|
|
0b60da3d6d | ||
|
|
a4492b90e7 | ||
|
|
c9331b7338 | ||
|
|
4982ea53dd | ||
|
|
c703cb6504 | ||
|
|
a7494971fd | ||
|
|
c548255bfc | ||
|
|
9656c7afd7 | ||
|
|
414b8c8272 | ||
|
|
b4f83d09d4 | ||
|
|
67ee74ac20 | ||
|
|
93539d2b6b | ||
|
|
524d967745 | ||
|
|
0effbebbb6 | ||
|
|
dca07a70f8 | ||
|
|
02936c97fd | ||
|
|
8f7e732827 | ||
|
|
5ffe563b7d | ||
|
|
6ef08c3d54 | ||
|
|
908b4b58cd | ||
|
|
f2611e0de0 | ||
|
|
ea75a9d351 | ||
|
|
3a744d7d68 | ||
|
|
195d312ae2 | ||
|
|
76006a7377 | ||
|
|
11536da512 | ||
|
|
2a91bc41f7 | ||
|
|
baf23157fc | ||
|
|
83b81be825 | ||
|
|
4aefa0f1f7 | ||
|
|
e53a7cee97 | ||
|
|
8437e1aa7b | ||
|
|
632f40cc0a | ||
|
|
7d81445341 | ||
|
|
78a5f401d7 | ||
|
|
8745f98c95 | ||
|
|
f0f5bcc630 | ||
|
|
8a3c513605 | ||
|
|
145d2636dd | ||
|
|
f2b9dc8988 | ||
|
|
2e4d56728b | ||
|
|
18231ed324 | ||
|
|
d0b61d8ed1 | ||
|
|
d385a44949 | ||
|
|
d97392d46e | ||
|
|
6abb2c73fd | ||
|
|
7e141c6b36 | ||
|
|
53553c1f62 | ||
|
|
523ccc6bf8 | ||
|
|
811e89fcfa | ||
|
|
5d5be4d9d7 | ||
|
|
88457ab139 | ||
|
|
0034926df7 | ||
|
|
d082d41ab9 | ||
|
|
b7911475b6 | ||
|
|
672754b0b5 | ||
|
|
0d1553123b | ||
|
|
ba6c51c102 | ||
|
|
d64206a9ff | ||
|
|
d9a1089039 | ||
|
|
55fe463405 | ||
|
|
e84210e962 | ||
|
|
ff506548d3 | ||
|
|
f6b09751e9 | ||
|
|
3d863979c4 | ||
|
|
2947ff4131 | ||
|
|
b8fca10896 | ||
|
|
33e45794d2 | ||
|
|
42cc88ca65 | ||
|
|
0b7f2416ca | ||
|
|
5d5c745ee5 | ||
|
|
e0429e4c60 | ||
|
|
0bece5287e | ||
|
|
60b5e47836 | ||
|
|
aa75b44790 | ||
|
|
769f58caa9 | ||
|
|
e7facf740d | ||
|
|
04921eef62 | ||
|
|
8863c42879 | ||
|
|
2745116ac5 | ||
|
|
bafe1c5fee | ||
|
|
306d7b2ce0 | ||
|
|
e9f6583c60 | ||
|
|
42a2835929 | ||
|
|
c2c90c680e | ||
|
|
cd01f6378c | ||
|
|
6033075de6 | ||
|
|
79794d3441 | ||
|
|
031f86b417 | ||
|
|
891f53cf6f | ||
|
|
848991cf5b | ||
|
|
d37ddd1d41 | ||
|
|
00d12acd5e | ||
|
|
3bbc78a44f | ||
|
|
b0a6652cc6 | ||
|
|
cb710b2e5f | ||
|
|
ca5fe6f7db | ||
|
|
fb75f4c68b | ||
|
|
5e2a418485 | ||
|
|
24fe215067 | ||
|
|
ab2e8875ac | ||
|
|
dec5740c74 | ||
|
|
208266dfa3 | ||
|
|
32f218d58c | ||
|
|
6fdaab2ccd | ||
|
|
d336866f44 | ||
|
|
b40df5f1c4 | ||
|
|
3c9886ad1b | ||
|
|
ea205ebd12 | ||
|
|
30dad46c94 | ||
|
|
fbf79e62e9 | ||
|
|
efcf72bc08 | ||
|
|
3b511e2f55 | ||
|
|
e4e20fb43a | ||
|
|
48ccff67a6 | ||
|
|
a783d6507b | ||
|
|
fd94e60797 | ||
|
|
a1bcb7ea30 | ||
|
|
31b67164c7 | ||
|
|
786c13f892 | ||
|
|
c652659d54 | ||
|
|
ca39196f13 | ||
|
|
f02dd8fd4b | ||
|
|
0f89886ce7 | ||
|
|
1118404192 | ||
|
|
f011ea6cce | ||
|
|
b2ac9c6c1a | ||
|
|
fbab41abd6 | ||
|
|
82f881af5b | ||
|
|
68de9b437d | ||
|
|
830a715b6d | ||
|
|
ce4aca9a72 | ||
|
|
7641171a01 | ||
|
|
119e084e52 | ||
|
|
7c6d52913e | ||
|
|
f63ab5cf7c | ||
|
|
50f1bc5017 | ||
|
|
c3ab409b6a | ||
|
|
44f6ab4878 | ||
|
|
5fda6e0f12 | ||
|
|
38068e78c9 | ||
|
|
66d22727e9 | ||
|
|
db2f68e35d | ||
|
|
352277ec15 | ||
|
|
d6043e64f2 | ||
|
|
d3f5b8f32e | ||
|
|
6c3c722674 | ||
|
|
5b8edb13d8 | ||
|
|
c595727b94 | ||
|
|
d46302588a | ||
|
|
0ff9fdb365 | ||
|
|
e95f7ce367 | ||
|
|
df1a8f4066 | ||
|
|
32e6c1660f | ||
|
|
d6b9b72e9b | ||
|
|
179ad03fa4 | ||
|
|
c3cb82c84e | ||
|
|
4b52e2ed9e | ||
|
|
77fd61f81e | ||
|
|
c3ffb7f83b | ||
|
|
89dcd72d70 | ||
|
|
5c3346aa9d | ||
|
|
7c4b383477 | ||
|
|
bdc0e8e0fc | ||
|
|
6d66f93565 | ||
|
|
9cac93b724 | ||
|
|
0709f263af | ||
|
|
4e4effd8b1 | ||
|
|
f9632cba61 | ||
|
|
38db6a41d5 | ||
|
|
7c6f0432c8 | ||
|
|
56ff9368be | ||
|
|
597e21d44d | ||
|
|
5bf54632be | ||
|
|
3a8d3ee515 | ||
|
|
1c1cf866e2 | ||
|
|
ccc1df75f1 | ||
|
|
d2c3f87656 | ||
|
|
6d62229b5f | ||
|
|
7c88865d67 | ||
|
|
c8cfe0cb5a | ||
|
|
e573bdba92 | ||
|
|
d8cd15d361 | ||
|
|
1db3907838 | ||
|
|
72cfd37ab7 | ||
|
|
1e67ee995e | ||
|
|
6c26b4080c | ||
|
|
0dbd59b223 | ||
|
|
b2066c60d1 | ||
|
|
8d7ae324ff | ||
|
|
c0d3c4f875 | ||
|
|
27a771648a | ||
|
|
86affc7304 | ||
|
|
d939b99628 | ||
|
|
1fcf777f3d | ||
|
|
7a8e23faa9 | ||
|
|
73a4dd3321 | ||
|
|
13ce873a69 | ||
|
|
406dc64aba | ||
|
|
af5d6a2015 | ||
|
|
61c6f509ae | ||
|
|
98769ecd88 | ||
|
|
8615950bd6 | ||
|
|
1bec8dfc48 | ||
|
|
460486fe25 | ||
|
|
318c50bc6c | ||
|
|
3e08bac7f3 | ||
|
|
c3d64ab185 | ||
|
|
2b73077b50 | ||
|
|
f953bd5488 | ||
|
|
f94011cf05 | ||
|
|
aeacf109eb | ||
|
|
e307de83e2 | ||
|
|
85968ec417 | ||
|
|
993f14a31f | ||
|
|
566d617508 | ||
|
|
542a279fcb | ||
|
|
e784bb89e1 | ||
|
|
f680ace258 | ||
|
|
7aa5976e07 | ||
|
|
f88f1ea951 | ||
|
|
da4561cb35 | ||
|
|
1f89ae9813 | ||
|
|
5647323449 | ||
|
|
bc27253cbf | ||
|
|
0672b711f3 | ||
|
|
ed9ee6e347 | ||
|
|
7ad23ad4a2 | ||
|
|
8a83f03cc1 | ||
|
|
0be9ac4097 | ||
|
|
ba5be6b516 | ||
|
|
c4aea6d326 | ||
|
|
858c6407a9 | ||
|
|
c4313395b5 | ||
|
|
a32aec3d59 | ||
|
|
696bcfe8fa | ||
|
|
2f3a253c6a | ||
|
|
e41fbe0188 | ||
|
|
ef9d28597b | ||
|
|
6f3c4c89ab | ||
|
|
60c577a61e | ||
|
|
f3276c3039 | ||
|
|
37a843323d | ||
|
|
95c780ca8c | ||
|
|
d60d5b154a | ||
|
|
0435a805c7 | ||
|
|
f406a977e0 | ||
|
|
18db1e1ecb | ||
|
|
6bd1beb719 | ||
|
|
1293aecbca | ||
|
|
8a10c2e112 | ||
|
|
c21d777269 | ||
|
|
d864094f48 | ||
|
|
deaac3fdf0 | ||
|
|
b7062fe40c | ||
|
|
64d5e99b9d | ||
|
|
f9d8a7d22b | ||
|
|
52fcd3ad98 | ||
|
|
9d1e0ee29b | ||
|
|
de62f48f50 | ||
|
|
f47b19274c | ||
|
|
bb7f7083b9 | ||
|
|
cd580090dc | ||
|
|
ddb74b598d | ||
|
|
29571fc3aa | ||
|
|
57ee0fb2bd | ||
|
|
3ef10e73a5 | ||
|
|
dc40492fc7 | ||
|
|
e606a76a86 | ||
|
|
8838fd67b9 | ||
|
|
c570e20308 | ||
|
|
0a00ef39e3 | ||
|
|
9a08b81214 | ||
|
|
c617ae26a2 | ||
|
|
f6a776a692 | ||
|
|
54b253099d | ||
|
|
f662aca58c | ||
|
|
76e7755496 | ||
|
|
e05ad81c13 | ||
|
|
cffb16d7f7 | ||
|
|
18ca571944 | ||
|
|
3ae1973e21 | ||
|
|
308c8c3ea7 | ||
|
|
f49b5dd037 | ||
|
|
f245ba82ad | ||
|
|
60d22d6973 | ||
|
|
d6f48a82d9 | ||
|
|
c0d73dae67 | ||
|
|
49eb60589d | ||
|
|
89993b7421 | ||
|
|
511cb93806 | ||
|
|
8ce78e7134 | ||
|
|
9ebfab2e78 | ||
|
|
833d245251 | ||
|
|
00d3024143 | ||
|
|
aedeab8a6a | ||
|
|
4d39169eb8 | ||
|
|
2ddc448150 | ||
|
|
f9a6b4ce2c | ||
|
|
22b2b69413 | ||
|
|
7f11632ea6 | ||
|
|
c0b4d5e2c2 | ||
|
|
2c23d0249c | ||
|
|
c3233fbf61 | ||
|
|
ecfc8e208c | ||
|
|
52d5e21fc4 | ||
|
|
6d0c56554f | ||
|
|
844e91dc9e | ||
|
|
1f00b5f577 | ||
|
|
2c48458384 | ||
|
|
ddda87c5a7 | ||
|
|
6b1bbca620 | ||
|
|
b5378e5d3c | ||
|
|
c69a55df29 | ||
|
|
5faa1a993a | ||
|
|
e56481f6d7 | ||
|
|
f9610d457c | ||
|
|
ae066f42a4 | ||
|
|
c60dd42fa7 | ||
|
|
7aac5ac5a1 | ||
|
|
ad0f3fa33b | ||
|
|
63d121b796 | ||
|
|
4291cfe82f | ||
|
|
f312868154 | ||
|
|
5b42d34ac8 | ||
|
|
397a8c275d | ||
|
|
2aabee453b | ||
|
|
185333a615 | ||
|
|
7d177eb1d4 | ||
|
|
705a84051d | ||
|
|
f6821f80e1 | ||
|
|
e7a6f5228d | ||
|
|
8161fd6acb | ||
|
|
2137920e81 | ||
|
|
879102599c | ||
|
|
44190f07fe | ||
|
|
a41487eb8f | ||
|
|
e1acaaa27c | ||
|
|
08a97aeff8 | ||
|
|
5b7302b46d | ||
|
|
34c0bba130 | ||
|
|
5a53447272 | ||
|
|
b6847289ff | ||
|
|
d22c43e08b | ||
|
|
d9deaa8d74 | ||
|
|
6c7776a9a6 | ||
|
|
62bd6e41ef | ||
|
|
293c7b42c6 | ||
|
|
788da62777 | ||
|
|
2c7f24a913 | ||
|
|
f236706d6a | ||
|
|
b097700591 | ||
|
|
50b112c9d6 | ||
|
|
c2f478b088 | ||
|
|
dccbb137d7 | ||
|
|
90f9940dbd | ||
|
|
f3f7cc9077 | ||
|
|
c331e2f39e | ||
|
|
1c7ebc4323 | ||
|
|
5f5427266f | ||
|
|
33e655becd | ||
|
|
0ea0602aec | ||
|
|
46effd2ca4 | ||
|
|
de055e8260 | ||
|
|
c3077304af | ||
|
|
e15135911f | ||
|
|
d430cae944 | ||
|
|
f92dc6f71b | ||
|
|
a679be68b1 | ||
|
|
c5c5ce8409 | ||
|
|
e7cb0d397e | ||
|
|
b84308cb49 | ||
|
|
0df47d2ce3 | ||
|
|
e24b548b54 | ||
|
|
75af444cee | ||
|
|
02dd19962f | ||
|
|
f552b8ef7b | ||
|
|
9162e31489 | ||
|
|
01b28e3ee8 | ||
|
|
f5aa855125 | ||
|
|
db3610fcdb | ||
|
|
2e3f330058 | ||
|
|
1617a7f2c1 | ||
|
|
69a5566bf9 | ||
|
|
30e5d8b855 | ||
|
|
67ff7726e0 | ||
|
|
f96a2e2325 | ||
|
|
344c4f9385 | ||
|
|
89aa146845 | ||
|
|
468e569bc7 | ||
|
|
139c99001a | ||
|
|
bd99be15c2 | ||
|
|
1d91d8fd94 | ||
|
|
f425f86101 | ||
|
|
83a6b7567f | ||
|
|
9184c70883 | ||
|
|
f5ca4ccce5 | ||
|
|
50f174be92 | ||
|
|
e5d11ce535 | ||
|
|
94851a51aa | ||
|
|
cfc07f4411 | ||
|
|
c6e9abda9f | ||
|
|
25951ddc55 | ||
|
|
bcd9ece077 | ||
|
|
68adbc38ba | ||
|
|
79a4d06cc0 | ||
|
|
18bf3b7548 | ||
|
|
4e66d3532e | ||
|
|
1b6d567451 | ||
|
|
7959a79575 | ||
|
|
abf3249b67 | ||
|
|
35e0dc84e8 | ||
|
|
17639e8729 | ||
|
|
cbd1fd908c | ||
|
|
b2cf20f3d8 | ||
|
|
915f1a5036 | ||
|
|
a55ec6416c | ||
|
|
b1834b1958 | ||
|
|
1beeb9fb55 | ||
|
|
18d86354ec | ||
|
|
6297b0679c | ||
|
|
d62ef635a7 | ||
|
|
c53836040f | ||
|
|
0b638bf85f | ||
|
|
7f6a71b964 | ||
|
|
1b4363a54a | ||
|
|
16d168c970 | ||
|
|
4606d7960e | ||
|
|
4eee126d26 | ||
|
|
dde426658f | ||
|
|
f6874fbcad | ||
|
|
621d4e4d92 | ||
|
|
76062231fd | ||
|
|
261f55fea5 | ||
|
|
202cf4bcc9 | ||
|
|
b7572f727f | ||
|
|
50ab346d58 | ||
|
|
b11b375848 | ||
|
|
e6c3ae9397 | ||
|
|
df663aceb9 | ||
|
|
db7e597f67 | ||
|
|
1d3fe81ff7 | ||
|
|
9c887fbe63 | ||
|
|
4723bffcd2 | ||
|
|
9643de3ca0 | ||
|
|
3bf3a54916 | ||
|
|
bcffc8856a | ||
|
|
6b8c35c27b | ||
|
|
dd409b4d1c | ||
|
|
94a1aebe2b | ||
|
|
d3030c3ec6 | ||
|
|
0221021078 | ||
|
|
966021bfd4 | ||
|
|
f06e6e85d5 | ||
|
|
28ad641070 | ||
|
|
384c775f1a | ||
|
|
ce40c691e9 | ||
|
|
5b0c38b0ed | ||
|
|
734456785f | ||
|
|
4f24312432 | ||
|
|
d79b1ff3b4 | ||
|
|
bbe1c1f1e0 | ||
|
|
1978e67401 | ||
|
|
e129e4a2d0 | ||
|
|
f7f1bbbdd2 | ||
|
|
de8f2e6a68 | ||
|
|
85704e3947 | ||
|
|
4d661ff41d | ||
|
|
d7b39634e6 | ||
|
|
039c98b9e3 | ||
|
|
172c4bf0a9 | ||
|
|
1f2a1c5dec | ||
|
|
e5a6a00282 | ||
|
|
d8153f7611 | ||
|
|
8b6ae3f39b | ||
|
|
24537781b7 | ||
|
|
d2a29506aa | ||
|
|
adf51d5264 | ||
|
|
0864179085 | ||
|
|
8de77f283d | ||
|
|
004a014000 | ||
|
|
80f6eb94aa | ||
|
|
4035c9cc5f | ||
|
|
3a365f6807 | ||
|
|
9920a0a59f | ||
|
|
c17bb9e171 | ||
|
|
03073f6875 | ||
|
|
609caf6e5f | ||
|
|
411141ff88 | ||
|
|
3e472e18bd | ||
|
|
e5b6fbd12a | ||
|
|
c2787f1282 | ||
|
|
df940124b1 | ||
|
|
5288d042ca | ||
|
|
fa98a27c90 | ||
|
|
d341a5a60b | ||
|
|
7f15227de1 | ||
|
|
bb45240665 | ||
|
|
29f84aeab5 | ||
|
|
5a52edcad8 | ||
|
|
b078e23aa1 | ||
|
|
7fa87125b5 | ||
|
|
f618df46d8 | ||
|
|
ee03853901 | ||
|
|
6c4a9bcfb8 | ||
|
|
1bec20ecef | ||
|
|
08c9bf570d | ||
|
|
5e77a10a81 | ||
|
|
3bc6461e2a | ||
|
|
d3194e15e2 | ||
|
|
2db79ef202 | ||
|
|
b3c07edef6 | ||
|
|
b773fdca34 | ||
|
|
2e9f9f7b7e | ||
|
|
30cbfe729d | ||
|
|
b036da2446 | ||
|
|
c8a9fb1674 | ||
|
|
43bea80cad | ||
|
|
23538c0323 | ||
|
|
2ae911230d | ||
|
|
5ce1cb87ea | ||
|
|
2a37028b6a | ||
|
|
8130feb2a0 | ||
|
|
c49a875ec2 | ||
|
|
2a002304b9 | ||
|
|
d9522818ae | ||
|
|
800588e121 | ||
|
|
991c31ebdb | ||
|
|
48f77e1691 | ||
|
|
42de6fd074 | ||
|
|
62845b470c | ||
|
|
fd20986cf8 | ||
|
|
61369cde9e | ||
|
|
644384ce8b | ||
|
|
97c11a2482 | ||
|
|
1e7e1c2d78 | ||
|
|
1c7201fb04 | ||
|
|
61ec0c697a | ||
|
|
4b5fce1bfc | ||
|
|
6cc6e7c8e9 | ||
|
|
89298fce30 | ||
|
|
a3a27e07fa | ||
|
|
4f32376f22 | ||
|
|
58bf189941 | ||
|
|
bcfa508da5 | ||
|
|
c0ae3ef58b | ||
|
|
1e70d7b4c3 | ||
|
|
f8dc6ad2bc | ||
|
|
e22482988f | ||
|
|
4eb896629d | ||
|
|
b310e66275 | ||
|
|
b39da1bea7 | ||
|
|
fa575d0574 | ||
|
|
dfe2f3771b | ||
|
|
46caeb0445 | ||
|
|
59cc9c7006 | ||
|
|
12e91534eb | ||
|
|
d9da88ceb5 | ||
|
|
2dbfec0307 | ||
|
|
09cf8c9641 | ||
|
|
f1bed4d6a3 | ||
|
|
2ed6c33c83 | ||
|
|
7ad532ed17 | ||
|
|
92fe8c5b14 | ||
|
|
8e95572589 | ||
|
|
62da862a66 | ||
|
|
993e34f548 | ||
|
|
e39465aece | ||
|
|
8fd616b680 | ||
|
|
cc054b27de | ||
|
|
dfdaa82245 | ||
|
|
99a307e0ad | ||
|
|
5ddea836a1 | ||
|
|
208d92aa06 | ||
|
|
6ef9ddd4f3 | ||
|
|
1c92d39185 | ||
|
|
c0f072217c | ||
|
|
542562f988 | ||
|
|
4e6f0d5e87 | ||
|
|
10639a5ead | ||
|
|
06d668e710 | ||
|
|
d1472dfcba | ||
|
|
ccb4da3cd8 | ||
|
|
46e96b49f0 | ||
|
|
984cfe7f98 | ||
|
|
d769300137 | ||
|
|
d175d66828 | ||
|
|
c1a314332e | ||
|
|
046ac59d21 | ||
|
|
00c06f07d0 | ||
|
|
3e2ab40c6a | ||
|
|
350ffd0052 | ||
|
|
ecd1a622d2 | ||
|
|
f13968aa61 | ||
|
|
4d1ffde54c | ||
|
|
d69017a706 | ||
|
|
f2deaeccdb | ||
|
|
ea9b0d2a79 | ||
|
|
2e6dbedb8b | ||
|
|
6f359df8f9 | ||
|
|
f6db20cd06 | ||
|
|
6287fae065 | ||
|
|
e441607ce3 | ||
|
|
b5379a95fa | ||
|
|
64ec5be919 | ||
|
|
3916512d66 | ||
|
|
e2f426a1bd | ||
|
|
aa1df8dfcf | ||
|
|
67557555f2 | ||
|
|
4cb652abd9 | ||
|
|
d11868b99f | ||
|
|
1798417e6a | ||
|
|
43dc3e5bb1 | ||
|
|
91891a14ed | ||
|
|
20f7d60147 | ||
|
|
7e17e7d37a | ||
|
|
cbb244f785 | ||
|
|
1c264d858b | ||
|
|
217037c2ae | ||
|
|
b4dbd0b69c | ||
|
|
89a2b5c00b | ||
|
|
929b6dae1a | ||
|
|
52fe493da9 | ||
|
|
3e6be3e762 | ||
|
|
7a8cc449b9 | ||
|
|
8f5a9d6e9f | ||
|
|
1c5e31fea9 | ||
|
|
fd08ae18ab | ||
|
|
a7eb3de06e | ||
|
|
8902dd7c44 | ||
|
|
6387d8400c | ||
|
|
597cacb9cc | ||
|
|
3e285ad9ff | ||
|
|
cc1fa89790 | ||
|
|
b0ed007751 | ||
|
|
e1e2650d2b | ||
|
|
b23f17b633 | ||
|
|
818e40b2df | ||
|
|
5685e39631 | ||
|
|
72534b7674 | ||
|
|
328490d23d | ||
|
|
97a0696930 | ||
|
|
cb4e0660e0 | ||
|
|
67c642de4c | ||
|
|
0d7c2e1024 | ||
|
|
16a779a41b | ||
|
|
c4ca3c8644 | ||
|
|
aabcbe34f3 | ||
|
|
f06626e441 | ||
|
|
c4e1a71776 | ||
|
|
77e6c16bd2 | ||
|
|
9d1fac3570 | ||
|
|
b7aeaa7fc5 | ||
|
|
f6d8c9ff61 | ||
|
|
0490794d6c | ||
|
|
335c83dd3c | ||
|
|
91da720c26 | ||
|
|
b6ac744a68 | ||
|
|
526c4092fd | ||
|
|
ed06dda384 | ||
|
|
6465b11e9b | ||
|
|
b2879878a1 | ||
|
|
3e17b086fb | ||
|
|
0545e6bcda | ||
|
|
27a907433f | ||
|
|
69616800e3 | ||
|
|
abf1f53432 | ||
|
|
881c5f75cb | ||
|
|
4e45796ade | ||
|
|
1ce4ea5230 | ||
|
|
f2a2437baa | ||
|
|
508dc9db1e | ||
|
|
a914e3557f | ||
|
|
f489dc062f | ||
|
|
a7e09f4850 | ||
|
|
8ea97530d4 | ||
|
|
13ab54e83a | ||
|
|
4bc40325cb | ||
|
|
58d9355ea3 | ||
|
|
d46b7528e7 | ||
|
|
1858597fc9 | ||
|
|
83cce5afe4 | ||
|
|
201bd8dc1f | ||
|
|
b62ba69060 | ||
|
|
5d2f5557e5 | ||
|
|
cf75c1aad0 | ||
|
|
76a60df88b | ||
|
|
9322c79b4e | ||
|
|
12365edcf0 | ||
|
|
5efc1f9dad | ||
|
|
ab976cbb24 | ||
|
|
db584b7897 | ||
|
|
0fdc0748cf | ||
|
|
2e79c21dc2 | ||
|
|
5490a230bd | ||
|
|
a6b059b30d | ||
|
|
712e6011aa | ||
|
|
68f6f87410 | ||
|
|
50cdd68b7b | ||
|
|
e8510b925e | ||
|
|
24e800501a |
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
[*.sh]
|
||||
# like -i=4
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
[*.nix]
|
||||
# like -i=4
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if [[ -z "${POEDITOR_API_TOKEN:-}" ]] || [[ -z "${POEDITOR_PROJECT_ID:-}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
||||
echo "Translations out of sync"
|
||||
echo "run python3 scripts/i18nsync.py sync"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,20 +6,12 @@ labels: "bug"
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
<!-- If your issue is related to ICONS
|
||||
<!-- If your issue is related to ICONS
|
||||
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
||||
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
||||
- Follow the [THEMING](https://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
|
||||
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
|
||||
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
||||
|
||||
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
|
||||
Quickshell does not ever update its DesktopEntires.
|
||||
There is an open PR for it, that has been stuck unmerged over there to fix it.
|
||||
We unfortunately are at the mercy of quickshell to merge it.
|
||||
Until then, newly installed and removed apps will not react until the
|
||||
shell is restarted.
|
||||
-->
|
||||
|
||||
## Compositor
|
||||
|
||||
- [ ] niri
|
||||
@@ -70,4 +62,4 @@ Paste error messages or logs here
|
||||
|
||||
## Screenshots/Recordings
|
||||
|
||||
<!-- If applicable, add screenshots or screen recordings -->
|
||||
<!-- If applicable, add screenshots or screen recordings -->
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -30,4 +30,4 @@ Is this feature specific to one compositor?
|
||||
|
||||
## Alternatives/Existing Solutions
|
||||
|
||||
<!-- Include any similar/pre-existing products that solve this problem -->
|
||||
<!-- Include any similar/pre-existing products that solve this problem -->
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/support_request.md
vendored
2
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -37,4 +37,4 @@ assignees: ""
|
||||
|
||||
## Screenshots/Recordings
|
||||
|
||||
<!-- If applicable, add screenshots or screen recordings -->
|
||||
<!-- If applicable, add screenshots or screen recordings -->
|
||||
|
||||
383
.github/workflows/backup/run-obs.yml.bak
vendored
Normal file
383
.github/workflows/backup/run-obs.yml.bak
vendored
Normal file
@@ -0,0 +1,383 @@
|
||||
name: Update OBS Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to update (dms, dms-git, or all)"
|
||||
required: false
|
||||
default: "all"
|
||||
force_upload:
|
||||
description: "Force upload without version check"
|
||||
required: false
|
||||
default: "false"
|
||||
type: choice
|
||||
options:
|
||||
- "false"
|
||||
- "true"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
default: ""
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
schedule:
|
||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||
|
||||
jobs:
|
||||
check-updates:
|
||||
name: Check for updates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||
packages: ${{ steps.check.outputs.packages }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install OSC
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y osc
|
||||
|
||||
mkdir -p ~/.config/osc
|
||||
cat > ~/.config/osc/oscrc << EOF
|
||||
[general]
|
||||
apiurl = https://api.opensuse.org
|
||||
|
||||
[https://api.opensuse.org]
|
||||
user = ${{ secrets.OBS_USERNAME }}
|
||||
pass = ${{ secrets.OBS_PASSWORD }}
|
||||
EOF
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION (always update)"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "Checking if dms-git source has changed..."
|
||||
|
||||
# Get current commit hash (8 chars to match spec format)
|
||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
|
||||
# Check OBS for last uploaded commit
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
mkdir -p "$OBS_BASE"
|
||||
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||
|
||||
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||
osc up -q 2>/dev/null || true
|
||||
|
||||
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
||||
if [[ -f "dms-git.spec" ]]; then
|
||||
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$OBS_COMMIT" ]]; then
|
||||
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 Could not extract OBS commit, proceeding with update"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 No spec file in OBS, proceeding with update"
|
||||
fi
|
||||
|
||||
cd "${{ github.workspace }}"
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 First upload to OBS, update needed"
|
||||
fi
|
||||
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Force upload: all packages"
|
||||
else
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Force upload: $PKG"
|
||||
fi
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
else
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
update-obs:
|
||||
name: Upload to OBS
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: |
|
||||
github.event.inputs.force_upload == 'true' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
needs.check-updates.outputs.has_updates == 'true'
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Check if last commit was automated
|
||||
id: check-loop
|
||||
run: |
|
||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
||||
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
||||
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "✅ Last commit was not automated, proceeding"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Determine packages to update
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
id: packages
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by schedule: updating git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
||||
run: |
|
||||
# Get commit info for dms-git versioning
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||
|
||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||
|
||||
# Update version in spec
|
||||
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||
|
||||
# Add changelog entry
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
||||
|
||||
- name: Update Debian dms-git changelog version
|
||||
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
||||
run: |
|
||||
# Get commit info for dms-git versioning
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||
|
||||
# Debian version format: 0.6.2+git2256.9162e314
|
||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
|
||||
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
||||
|
||||
# Get current version from changelog
|
||||
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
||||
|
||||
echo "Current Debian version: $CURRENT_VERSION"
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Only update if version changed
|
||||
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||
# Create new changelog entry at top
|
||||
TEMP_CHANGELOG=$(mktemp)
|
||||
|
||||
cat > "$TEMP_CHANGELOG" << EOF
|
||||
dms-git ($NEW_VERSION) nightly; urgency=medium
|
||||
|
||||
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||
|
||||
EOF
|
||||
|
||||
# Prepend to existing changelog
|
||||
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
||||
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
||||
|
||||
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
||||
else
|
||||
echo "✓ Debian changelog already at version $NEW_VERSION"
|
||||
fi
|
||||
|
||||
- name: Update dms stable version
|
||||
if: steps.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
|
||||
run: |
|
||||
VERSION="${{ steps.packages.outputs.version }}"
|
||||
VERSION_NO_V="${VERSION#v}"
|
||||
echo "Updating packaging to version $VERSION_NO_V"
|
||||
|
||||
# Update openSUSE dms spec (stable only)
|
||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||
|
||||
# Update openSUSE spec changelog
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
|
||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
|
||||
|
||||
# Update Debian _service files (both tar_scm and download_url formats)
|
||||
for service in distro/debian/*/_service; do
|
||||
if [[ -f "$service" ]]; then
|
||||
# Update tar_scm revision parameter (for dms-git)
|
||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||
|
||||
# Update download_url paths (for dms stable)
|
||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||
fi
|
||||
done
|
||||
|
||||
# Update Debian changelog for dms stable
|
||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
TEMP_CHANGELOG=$(mktemp)
|
||||
|
||||
cat > "$TEMP_CHANGELOG" << EOF
|
||||
dms ($VERSION_NO_V) stable; urgency=medium
|
||||
|
||||
* Update to $VERSION stable release
|
||||
* Bug fixes and improvements
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||
|
||||
EOF
|
||||
|
||||
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
|
||||
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
|
||||
|
||||
echo "✓ Updated Debian changelog to $VERSION_NO_V"
|
||||
fi
|
||||
|
||||
- name: Install Go
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Install OSC
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y osc
|
||||
|
||||
mkdir -p ~/.config/osc
|
||||
cat > ~/.config/osc/oscrc << EOF
|
||||
[general]
|
||||
apiurl = https://api.opensuse.org
|
||||
|
||||
[https://api.opensuse.org]
|
||||
user = ${{ secrets.OBS_USERNAME }}
|
||||
pass = ${{ secrets.OBS_PASSWORD }}
|
||||
EOF
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
|
||||
- name: Upload to OBS
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
env:
|
||||
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
MESSAGE="Automated update from GitHub Actions"
|
||||
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGES" == "all" ]]; then
|
||||
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||
else
|
||||
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
||||
fi
|
||||
|
||||
- name: Get changed packages
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
id: changed-packages
|
||||
run: |
|
||||
# Check if there are any changes to commit
|
||||
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 No changelog or spec changes to commit"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
# Get list of changed packages for commit message
|
||||
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
|
||||
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
|
||||
|
||||
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||
echo "packages=$PKGS" >> $GITHUB_OUTPUT
|
||||
echo "📋 Changed packages: $PKGS"
|
||||
fi
|
||||
|
||||
- name: Commit packaging changes
|
||||
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
|
||||
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
||||
git pull --rebase origin master
|
||||
git push
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
||||
298
.github/workflows/backup/run-ppa.yml.bak
vendored
Normal file
298
.github/workflows/backup/run-ppa.yml.bak
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
name: Update PPA Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||
required: false
|
||||
default: "dms-git"
|
||||
force_upload:
|
||||
description: "Force upload without version check"
|
||||
required: false
|
||||
default: "false"
|
||||
type: choice
|
||||
options:
|
||||
- "false"
|
||||
- "true"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||
required: false
|
||||
default: ""
|
||||
schedule:
|
||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||
|
||||
jobs:
|
||||
check-updates:
|
||||
name: Check for updates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||
packages: ${{ steps.check.outputs.packages }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "Checking if dms-git source has changed..."
|
||||
|
||||
# Get current commit hash (8 chars to match changelog format)
|
||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
|
||||
# Extract commit hash from changelog
|
||||
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
||||
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
||||
|
||||
if [[ -f "$CHANGELOG_FILE" ]]; then
|
||||
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
||||
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 No changelog file found, proceeding with upload"
|
||||
fi
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
else
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
upload-ppa:
|
||||
name: Upload to PPA
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: |
|
||||
github.event.inputs.force_upload == 'true' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
needs.check-updates.outputs.has_updates == 'true'
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Check if last commit was automated
|
||||
id: check-loop
|
||||
run: |
|
||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
||||
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
||||
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "✅ Last commit was not automated, proceeding"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set up Go
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache: false
|
||||
|
||||
- name: Install build dependencies
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
debhelper \
|
||||
devscripts \
|
||||
dput \
|
||||
lftp \
|
||||
build-essential \
|
||||
fakeroot \
|
||||
dpkg-dev
|
||||
|
||||
- name: Configure GPG
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
env:
|
||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_KEY" | gpg --import
|
||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine packages to upload
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
id: packages
|
||||
run: |
|
||||
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Force upload: all packages"
|
||||
else
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Force upload: $PKG"
|
||||
fi
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by schedule: uploading git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual package selection should respect change detection
|
||||
SELECTED_PKG="${{ github.event.inputs.package }}"
|
||||
UPDATED_PKG="${{ needs.check-updates.outputs.packages }}"
|
||||
|
||||
# Check if manually selected package is in the updated list
|
||||
if [[ "$UPDATED_PKG" == *"$SELECTED_PKG"* ]] || [[ "$SELECTED_PKG" == "all" ]]; then
|
||||
echo "packages=$SELECTED_PKG" >> $GITHUB_OUTPUT
|
||||
echo "📦 Manual selection (has updates): $SELECTED_PKG"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "⚠️ Manual selection '$SELECTED_PKG' has no updates - skipping (use force_upload to override)"
|
||||
fi
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Upload to PPA
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "No packages selected for upload. Skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build command arguments
|
||||
BUILD_ARGS=()
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
BUILD_ARGS+=("$REBUILD_RELEASE")
|
||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGES" == "all" ]]; then
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms to PPA..."
|
||||
if [ -n "$REBUILD_RELEASE" ]; then
|
||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms-git to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms-greeter to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
|
||||
else
|
||||
# Map package to PPA name
|
||||
case "$PACKAGES" in
|
||||
dms)
|
||||
PPA_NAME="dms"
|
||||
;;
|
||||
dms-git)
|
||||
PPA_NAME="dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PPA_NAME="danklinux"
|
||||
;;
|
||||
*)
|
||||
PPA_NAME="$PACKAGES"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading $PACKAGES to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
|
||||
fi
|
||||
|
||||
- name: Get changed packages
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
id: changed-packages
|
||||
run: |
|
||||
# Check if there are any changelog changes to commit
|
||||
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 No changelog changes to commit"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
# Get list of changed packages for commit message (deduplicate)
|
||||
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
|
||||
echo "📋 Changed packages: $CHANGED"
|
||||
echo "📋 Debug - Changed files:"
|
||||
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
|
||||
fi
|
||||
|
||||
- name: Commit changelog changes
|
||||
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
git add distro/ubuntu/*/debian/changelog
|
||||
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
||||
git pull --rebase origin master
|
||||
git push
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
if [[ "$PACKAGES" == "all" ]]; then
|
||||
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||
310
.github/workflows/copr-release.yml
vendored
310
.github/workflows/copr-release.yml
vendored
@@ -1,310 +0,0 @@
|
||||
name: DMS Copr Stable Release
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Create Release from DMS"]
|
||||
types: [completed]
|
||||
branches: [master]
|
||||
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
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
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 }}" = "workflow_run" ]; then
|
||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||
echo "Using latest release version from workflow_run: $VERSION"
|
||||
else
|
||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/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: MIT
|
||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||
|
||||
Source0: dms-qml.tar.gz
|
||||
|
||||
BuildRequires: gzip
|
||||
BuildRequires: wget
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
Requires: (quickshell or quickshell-git)
|
||||
Requires: accountsservice
|
||||
Requires: dms-cli
|
||||
Requires: dgop
|
||||
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: hyprpicker
|
||||
Recommends: matugen
|
||||
Recommends: wl-clipboard
|
||||
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: MIT
|
||||
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
|
||||
|
||||
# Shell completions
|
||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||
|
||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||
|
||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/*.spec
|
||||
|
||||
%posttrans
|
||||
# Clean up old installation path from previous versions (only if empty)
|
||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||
# Remove directories only if empty (preserves any user-added files)
|
||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Restart DMS for active users after upgrade
|
||||
if [ "$1" -ge 2 ]; then
|
||||
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md CONTRIBUTING.md
|
||||
%{_datadir}/quickshell/dms/
|
||||
%{_userunitdir}/dms.service
|
||||
|
||||
%files -n dms-cli
|
||||
%{_bindir}/dms
|
||||
%{_datadir}/bash-completion/completions/dms
|
||||
%{_datadir}/zsh/site-functions/_dms
|
||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||
|
||||
%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
|
||||
46
.github/workflows/go-ci.yml
vendored
Normal file
46
.github/workflows/go-ci.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Go CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths:
|
||||
- "core/**"
|
||||
- ".github/workflows/go-ci.yml"
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
paths:
|
||||
- "core/**"
|
||||
- ".github/workflows/go-ci.yml"
|
||||
|
||||
concurrency:
|
||||
group: go-ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: core
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build dms
|
||||
run: go build -v ./cmd/dms
|
||||
|
||||
- name: Build dms (distropkg)
|
||||
run: go build -v -tags distro_binary ./cmd/dms
|
||||
|
||||
- name: Build dankinstall
|
||||
run: go build -v ./cmd/dankinstall
|
||||
23
.github/workflows/nix-pr-check.yml
vendored
Normal file
23
.github/workflows/nix-pr-check.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Check nix flake
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
paths:
|
||||
- "flake.*"
|
||||
- "distro/nix/**"
|
||||
jobs:
|
||||
check-flake:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Check the flake
|
||||
run: nix flake check
|
||||
15
.github/workflows/prek.yml
vendored
Normal file
15
.github/workflows/prek.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Pre-commit Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
jobs:
|
||||
pre-commit-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: run pre-commit hooks
|
||||
uses: j178/prek-action@v1
|
||||
289
.github/workflows/release.yml
vendored
289
.github/workflows/release.yml
vendored
@@ -1,47 +1,202 @@
|
||||
# Release from a dispatch event from the danklinux repo
|
||||
name: Create Release from DMS
|
||||
name: Release
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [dms_release]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag to release (e.g., v1.0.1)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.event.client_payload.tag }}
|
||||
group: release-${{ inputs.tag }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
create_release_from_dms:
|
||||
runs-on: ubuntu-24.04
|
||||
build-core:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: core
|
||||
|
||||
env:
|
||||
TAG: ${{ github.event.client_payload.tag }}
|
||||
DMS_REPO: ${{ github.event.client_payload.dms_repo }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Ensure VERSION and tag
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
echo "${TAG}" > VERSION
|
||||
|
||||
git add -A VERSION
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "Update VERSION to ${TAG} (from DMS)"
|
||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||
echo "The following files are not formatted:"
|
||||
gofmt -s -l .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git tag -f "${TAG}"
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
||||
git push origin HEAD
|
||||
git push -f origin "${TAG}"
|
||||
- name: Build dankinstall (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dankinstall
|
||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dankinstall-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
||||
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Build dms (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dms
|
||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dms-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dms-${{ matrix.arch }}
|
||||
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Generate shell completions
|
||||
if: matrix.arch == 'amd64'
|
||||
run: |
|
||||
set -eux
|
||||
chmod +x dms-amd64
|
||||
./dms-amd64 completion bash > completion.bash
|
||||
./dms-amd64 completion fish > completion.fish
|
||||
./dms-amd64 completion zsh > completion.zsh
|
||||
|
||||
- name: Build dms-distropkg (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dms
|
||||
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
||||
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Upload artifacts (${{ matrix.arch }})
|
||||
if: matrix.arch == 'arm64'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
core/dankinstall-${{ matrix.arch }}.gz
|
||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-${{ matrix.arch }}.gz
|
||||
core/dms-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload artifacts with completions
|
||||
if: matrix.arch == 'amd64'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
core/dankinstall-${{ matrix.arch }}.gz
|
||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-${{ matrix.arch }}.gz
|
||||
core/dms-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
core/completion.bash
|
||||
core/completion.fish
|
||||
core/completion.zsh
|
||||
if-no-files-found: error
|
||||
|
||||
# update-versions:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: build-core
|
||||
# steps:
|
||||
# - name: Create GitHub App token
|
||||
# id: app_token
|
||||
# uses: actions/create-github-app-token@v1
|
||||
# with:
|
||||
# app-id: ${{ secrets.APP_ID }}
|
||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# token: ${{ steps.app_token.outputs.token }}
|
||||
# fetch-depth: 0
|
||||
|
||||
# - name: Update VERSION
|
||||
# env:
|
||||
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
# run: |
|
||||
# set -euo pipefail
|
||||
# git config user.name "dms-ci[bot]"
|
||||
# git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
|
||||
# version="${GITHUB_REF#refs/tags/}"
|
||||
# echo "Updating to version: $version"
|
||||
# echo "${version}" > quickshell/VERSION
|
||||
# git add quickshell/VERSION
|
||||
|
||||
# if ! git diff --cached --quiet; then
|
||||
# git commit -m "chore: bump version to $version"
|
||||
# git pull --rebase origin master
|
||||
# git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||
# fi
|
||||
|
||||
# git tag -f "${version}"
|
||||
# git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [build-core] #, update-versions]
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch updated tag after version bump
|
||||
run: |
|
||||
git fetch origin --force tag ${TAG}
|
||||
git checkout ${TAG}
|
||||
|
||||
- name: Download core artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: core-assets-*
|
||||
merge-multiple: true
|
||||
path: ./_core_assets
|
||||
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
@@ -55,6 +210,12 @@ jobs:
|
||||
fi
|
||||
|
||||
cat > RELEASE_BODY.md << 'EOF'
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.danklinux.com | sh
|
||||
```
|
||||
|
||||
## Assets
|
||||
|
||||
### Complete Packages
|
||||
@@ -66,6 +227,8 @@ jobs:
|
||||
- **`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
|
||||
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||
- **`dms-qml.tar.gz`** - QML source code only
|
||||
|
||||
### Checksums
|
||||
@@ -89,30 +252,14 @@ jobs:
|
||||
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create/Update DankMaterialShell Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.TAG }}
|
||||
name: Release ${{ env.TAG }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
draft: false
|
||||
prerelease: ${{ contains(env.TAG, '-') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download and prepare release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
mkdir -p _release_assets
|
||||
|
||||
# Download DMS CLI binaries from the danklinux repo
|
||||
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
|
||||
|
||||
# Rename CLI binaries to dms-cli-* format and copy distropkg binaries
|
||||
for file in _dms_assets/dms-*.gz*; do
|
||||
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
|
||||
for file in _core_assets/dms-*.gz*; do
|
||||
if [ -f "$file" ]; then
|
||||
basename=$(basename "$file")
|
||||
if [[ "$basename" == dms-distropkg-* ]]; then
|
||||
@@ -124,40 +271,62 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
# Create QML source package (exclude .git, .github, build artifacts)
|
||||
tar --exclude='.git' \
|
||||
# Copy dankinstall binaries
|
||||
cp _core_assets/dankinstall-*.gz* _release_assets/
|
||||
|
||||
# Copy completions
|
||||
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||
|
||||
# Create QML source package (exclude build artifacts and git files)
|
||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||
cp LICENSE CONTRIBUTING.md quickshell/
|
||||
|
||||
# Copy root assets directory to quickshell for systemd service and desktop file
|
||||
cp -r assets quickshell/
|
||||
|
||||
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||
(cd quickshell && tar --exclude='.git' \
|
||||
--exclude='.github' \
|
||||
--exclude='_dms_assets' \
|
||||
--exclude='_release_assets' \
|
||||
--exclude='*.tar.gz' \
|
||||
-czf _release_assets/dms-qml.tar.gz .
|
||||
-czf ../_release_assets/dms-qml.tar.gz .)
|
||||
|
||||
# Generate checksum for QML package
|
||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||
|
||||
# Create full packages for each architecture
|
||||
for arch in amd64 arm64; do
|
||||
mkdir -p _temp_full/dms
|
||||
mkdir -p _temp_full/bin
|
||||
mkdir -p _temp_full/completions
|
||||
|
||||
# Extract QML source
|
||||
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
||||
|
||||
if [ -f "_dms_assets/dms-${arch}.gz" ]; then
|
||||
gunzip -c "_dms_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||
# Add CLI binaries
|
||||
if [ -f "_core_assets/dms-${arch}.gz" ]; then
|
||||
gunzip -c "_core_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||
chmod +x _temp_full/bin/dms
|
||||
fi
|
||||
|
||||
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
|
||||
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
|
||||
gunzip -c "_core_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||
chmod +x _temp_full/bin/dms-distropkg
|
||||
fi
|
||||
|
||||
for completion in _dms_assets/completion.*; do
|
||||
# Add shell completions
|
||||
for completion in _core_assets/completion.*; do
|
||||
if [ -f "$completion" ]; then
|
||||
cp "$completion" _temp_full/completions/
|
||||
fi
|
||||
done
|
||||
cat > _temp_full/INSTALL.md << 'EOF'
|
||||
|
||||
# Copy docs directory
|
||||
if [ -d "docs" ]; then
|
||||
cp -r docs _temp_full/
|
||||
fi
|
||||
|
||||
# Create installation guide
|
||||
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
|
||||
# DankMaterialShell Installation
|
||||
|
||||
## Requirements
|
||||
@@ -204,10 +373,9 @@ jobs:
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Run with verbose output: `quickshell -v -p ~/.config/quickshell/dms`
|
||||
- Check logs in `~/.local/state/DankMaterialShell/`
|
||||
- Run with verbose output: `DMS_LOG_LEVEL=debug dms run`
|
||||
- Ensure all dependencies are installed
|
||||
EOF
|
||||
EOFINSTALL
|
||||
|
||||
# Create the full package
|
||||
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
||||
@@ -219,11 +387,14 @@ jobs:
|
||||
rm -rf _temp_full
|
||||
done
|
||||
|
||||
- name: Attach all assets to release
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.TAG }}
|
||||
name: Release ${{ env.TAG }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
files: _release_assets/**
|
||||
draft: false
|
||||
prerelease: ${{ contains(env.TAG, '-') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
210
.github/workflows/run-copr.yml
vendored
Normal file
210
.github/workflows/run-copr.yml
vendored
Normal file
@@ -0,0 +1,210 @@
|
||||
name: DMS Copr Stable Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package to build (dms, dms-greeter, or both)'
|
||||
required: false
|
||||
default: 'dms'
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- both
|
||||
version:
|
||||
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
||||
required: false
|
||||
default: ''
|
||||
release:
|
||||
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
|
||||
required: false
|
||||
default: '1'
|
||||
|
||||
jobs:
|
||||
determine-packages:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
packages: ${{ steps.set-packages.outputs.packages }}
|
||||
steps:
|
||||
- name: Set package list
|
||||
id: set-packages
|
||||
run: |
|
||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||
if [ "$PACKAGE_INPUT" = "both" ]; then
|
||||
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-and-upload:
|
||||
needs: determine-packages
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJSON(needs.determine-packages.outputs.packages) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
# Get version from manual input or latest release
|
||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
echo "Using manual version: $VERSION"
|
||||
else
|
||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||
echo "Using latest release version: $VERSION"
|
||||
fi
|
||||
|
||||
RELEASE="${{ github.event.inputs.release }}"
|
||||
if [ -z "$RELEASE" ]; then
|
||||
RELEASE="1"
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||
echo "✅ Building ${{ matrix.package }} version: $VERSION-$RELEASE"
|
||||
|
||||
- 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 binary will be downloaded during build based on target architecture"
|
||||
ls -lh
|
||||
|
||||
- name: Generate stable spec file
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
RELEASE="${{ steps.version.outputs.release }}"
|
||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
# Copy spec file from repository
|
||||
cp distro/fedora/${PACKAGE}.spec ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
|
||||
# Replace placeholders with actual values
|
||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
|
||||
echo "✅ Spec file generated for ${PACKAGE} v${VERSION}-${RELEASE}"
|
||||
echo ""
|
||||
echo "=== Spec file preview ==="
|
||||
head -40 ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
|
||||
- name: Build SRPM
|
||||
id: build
|
||||
run: |
|
||||
cd ~/rpmbuild/SPECS
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
echo "🔨 Building SRPM for ${PACKAGE}..."
|
||||
rpmbuild -bs ${PACKAGE}.spec
|
||||
|
||||
SRPM=$(ls ~/rpmbuild/SRPMS/${PACKAGE}-*.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: ${{ matrix.package }}-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: Determine Copr project
|
||||
id: copr_project
|
||||
run: |
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
if [ "$PACKAGE" = "dms" ]; then
|
||||
COPR_PROJECT="avengemedia/dms"
|
||||
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
||||
COPR_PROJECT="avengemedia/danklinux"
|
||||
else
|
||||
echo "❌ Unknown package: $PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
echo "copr_project=$COPR_PROJECT" >> $GITHUB_OUTPUT
|
||||
echo "✅ Copr project: $COPR_PROJECT"
|
||||
|
||||
- name: Upload to Copr
|
||||
run: |
|
||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
echo "🚀 Uploading ${PACKAGE} SRPM to ${COPR_PROJECT}..."
|
||||
echo " SRPM: $(basename $SRPM)"
|
||||
echo " Version: $VERSION"
|
||||
|
||||
BUILD_OUTPUT=$(copr-cli build "$COPR_PROJECT" "$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/${COPR_PROJECT}/build/$BUILD_ID/"
|
||||
else
|
||||
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
||||
fi
|
||||
|
||||
- name: Build summary
|
||||
if: always()
|
||||
run: |
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||
echo "### 🎉 ${PACKAGE} Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Package:** ${PACKAGE}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
||||
406
.github/workflows/run-obs.yml
vendored
Normal file
406
.github/workflows/run-obs.yml
vendored
Normal file
@@ -0,0 +1,406 @@
|
||||
name: Update OBS Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to update (dms, dms-git, or all)"
|
||||
required: false
|
||||
default: "all"
|
||||
tag_version:
|
||||
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
|
||||
required: false
|
||||
default: ""
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
default: ""
|
||||
schedule:
|
||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||
|
||||
jobs:
|
||||
check-updates:
|
||||
name: Check for updates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||
packages: ${{ steps.check.outputs.packages }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
env:
|
||||
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
|
||||
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
|
||||
run: |
|
||||
# Helper function to check dms-git commit
|
||||
check_dms_git() {
|
||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
|
||||
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-git: New commit $CURRENT_COMMIT (OBS has ${OBS_COMMIT:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check dms stable tag
|
||||
check_dms_stable() {
|
||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
|
||||
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push - always update stable package
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION (always update)"
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - check dms-git only
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
if check_dms_git; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow trigger
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
|
||||
if [[ -n "$REBUILD" ]]; then
|
||||
# Rebuild requested - always proceed
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
if check_dms_git; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms" ]]; then
|
||||
if check_dms_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: $PKG"
|
||||
fi
|
||||
else
|
||||
# Fallback - proceed
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
update-obs:
|
||||
name: Upload to OBS
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-updates.outputs.has_updates == 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push event - use the pushed tag
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - dms-git only
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by schedule: updating git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow dispatch
|
||||
|
||||
# Determine version for dms stable
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||
# For explicit dms selection, require tag_version
|
||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.tag_version }}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using specified tag: $VERSION"
|
||||
else
|
||||
echo "ERROR: tag_version is required when package=dms"
|
||||
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
# For "all", auto-detect if tag_version not specified
|
||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.tag_version }}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using specified tag: $VERSION"
|
||||
else
|
||||
# Auto-detect latest release for "all"
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Auto-detected latest release: $LATEST_TAG"
|
||||
else
|
||||
echo "ERROR: Could not auto-detect latest release"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||
else
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
fi
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
|
||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||
|
||||
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||
|
||||
# Single changelog entry (git snapshots don't need history)
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms-git.spec)
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1"
|
||||
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||
} > distro/opensuse/dms-git.spec
|
||||
|
||||
- name: Update Debian dms-git changelog version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
|
||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||
|
||||
# Single changelog entry (git snapshots don't need history)
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms-git (${NEW_VERSION}db1) nightly; urgency=medium"
|
||||
echo ""
|
||||
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms-git/debian/changelog"
|
||||
|
||||
- name: Update dms stable version
|
||||
if: steps.packages.outputs.version != ''
|
||||
run: |
|
||||
VERSION="${{ steps.packages.outputs.version }}"
|
||||
VERSION_NO_V="${VERSION#v}"
|
||||
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||
|
||||
# Update spec file
|
||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||
|
||||
# Verify the update
|
||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
|
||||
|
||||
# Single changelog entry (full history on OBS website)
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
||||
echo "- Update to stable $VERSION release"
|
||||
} > distro/opensuse/dms.spec
|
||||
|
||||
# Update Debian _service files (both tar_scm and download_url formats)
|
||||
for service in distro/debian/*/_service; do
|
||||
if [[ -f "$service" ]]; then
|
||||
# Update tar_scm revision parameter (for dms-git)
|
||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||
|
||||
# Update download_url paths (for dms stable)
|
||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||
fi
|
||||
done
|
||||
|
||||
# Update Debian changelog for dms stable (single entry, history on OBS website)
|
||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Update to $VERSION stable release"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms/debian/changelog"
|
||||
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
|
||||
fi
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Install OSC
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y osc
|
||||
|
||||
mkdir -p ~/.config/osc
|
||||
cat > ~/.config/osc/oscrc << EOF
|
||||
[general]
|
||||
apiurl = https://api.opensuse.org
|
||||
|
||||
[https://api.opensuse.org]
|
||||
user = ${{ secrets.OBS_USERNAME }}
|
||||
pass = ${{ secrets.OBS_PASSWORD }}
|
||||
EOF
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
|
||||
- name: Upload to OBS
|
||||
env:
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "✓ No packages need uploading. All up to date!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MESSAGE="Automated update from GitHub Actions"
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||
fi
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading $PKG to OBS..."
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
echo "🔄 Using rebuild release number: db$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [[ "$PKG" == "dms-git" ]]; then
|
||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||
else
|
||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### OBS Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for PKG in $PACKAGES; do
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-git)
|
||||
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
echo "**Rebuild Number:** db${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
280
.github/workflows/run-ppa.yml
vendored
Normal file
280
.github/workflows/run-ppa.yml
vendored
Normal file
@@ -0,0 +1,280 @@
|
||||
name: Update PPA Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||
required: false
|
||||
default: "dms-git"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||
required: false
|
||||
default: ""
|
||||
schedule:
|
||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||
|
||||
jobs:
|
||||
check-updates:
|
||||
name: Check for updates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||
packages: ${{ steps.check.outputs.packages }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
run: |
|
||||
# Helper function to check dms-git commit
|
||||
check_dms_git() {
|
||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
|
||||
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check stable package tag
|
||||
check_stable_package() {
|
||||
local PKG="$1"
|
||||
local PPA_NAME="$2"
|
||||
# Use git ls-remote to find the latest tag, sorted by version (descending)
|
||||
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
|
||||
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
|
||||
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - check dms-git only
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
if check_dms_git; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow trigger
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
|
||||
if [[ -n "$REBUILD" ]]; then
|
||||
# Rebuild requested - always proceed
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
|
||||
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
if check_dms_git; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms" ]]; then
|
||||
if check_stable_package "dms" "dms"; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||
if check_stable_package "dms-greeter" "danklinux"; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: $PKG"
|
||||
fi
|
||||
else
|
||||
# Fallback
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
upload-ppa:
|
||||
name: Upload to PPA
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-updates.outputs.has_updates == 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache: false
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
debhelper \
|
||||
devscripts \
|
||||
dput \
|
||||
lftp \
|
||||
build-essential \
|
||||
fakeroot \
|
||||
dpkg-dev
|
||||
|
||||
- name: Configure GPG
|
||||
env:
|
||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_KEY" | gpg --import
|
||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine packages to upload
|
||||
id: packages
|
||||
run: |
|
||||
# Use packages determined by check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "Triggered by schedule: uploading git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
|
||||
fi
|
||||
|
||||
- name: Upload to PPA
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "✓ No packages need uploading. All up to date!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Export REBUILD_RELEASE so ppa-build.sh can use it
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
export REBUILD_RELEASE
|
||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
# Map package to PPA name
|
||||
case "$PKG" in
|
||||
dms)
|
||||
PPA_NAME="dms"
|
||||
;;
|
||||
dms-git)
|
||||
PPA_NAME="dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PPA_NAME="danklinux"
|
||||
;;
|
||||
*)
|
||||
echo "⚠️ Unknown package: $PKG, skipping"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading $PKG to PPA $PPA_NAME..."
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for PKG in $PACKAGES; do
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-git)
|
||||
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-greeter)
|
||||
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
19
.github/workflows/stable.yml
vendored
Normal file
19
.github/workflows/stable.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Update stable branch
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
update-stable:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Push to stable branch
|
||||
run: git push origin HEAD:refs/heads/stable --force
|
||||
66
.github/workflows/update-vendor-hash.yml
vendored
Normal file
66
.github/workflows/update-vendor-hash.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Update Vendor Hash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "core/go.mod"
|
||||
- "core/go.sum"
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-vendor-hash:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create GitHub App token
|
||||
id: app_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Update vendorHash in flake.nix
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Attempting nix build to get new vendorHash..."
|
||||
if output=$(nix build .#dms-shell 2>&1); then
|
||||
echo "Build succeeded, no hash update needed"
|
||||
exit 0
|
||||
fi
|
||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
||||
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||
echo "Verifying build with new vendorHash..."
|
||||
nix build .#dms-shell
|
||||
echo "vendorHash updated successfully!"
|
||||
|
||||
- name: Commit and push vendorHash update
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! git diff --quiet flake.nix; then
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
git add flake.nix
|
||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||
git pull --rebase origin master
|
||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||
else
|
||||
echo "No changes to flake.nix"
|
||||
fi
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -27,7 +27,6 @@ qrc_*.cpp
|
||||
ui_*.h
|
||||
*.qmlc
|
||||
*.jsc
|
||||
Makefile*
|
||||
*build-*
|
||||
*.qm
|
||||
*.prl
|
||||
@@ -97,8 +96,15 @@ go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
.env*
|
||||
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
# .vscode/
|
||||
vim/
|
||||
|
||||
bin/
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv/
|
||||
|
||||
12
.pre-commit-config.yaml
Normal file
12
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.10.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
||||
17
CHANGELOG.MD
Normal file
17
CHANGELOG.MD
Normal file
@@ -0,0 +1,17 @@
|
||||
This file is more of a quick reference so I know what to account for before next releases.
|
||||
|
||||
# 1.2.0
|
||||
|
||||
- Added clipboard and clipboard history integration
|
||||
- Added swipe to dismiss notification popups and from center
|
||||
- Added paste from clipboard history view - requires wtype
|
||||
- Optimize surface damage of OSD & Toast
|
||||
- Add monitor configurator (niri, Hyprland, MangoWC)
|
||||
- **BREAKING** ghostty theme changed to ~/.config/ghostty/themes/danktheme
|
||||
- requires intervention and doc update
|
||||
- Added desktop widget plugins
|
||||
- dev guidance available
|
||||
- builtin clock & dgop widgets
|
||||
- new IPC targets
|
||||
- Initial RTL support/i18n
|
||||
- Theme registry
|
||||
@@ -2,28 +2,80 @@
|
||||
|
||||
Contributions are welcome and encouraged.
|
||||
|
||||
## Formatting
|
||||
To contribute fork this repository, make your changes, and open a pull request.
|
||||
|
||||
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported.
|
||||
## Setup
|
||||
|
||||
We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
|
||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
||||
|
||||
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
|
||||
|
||||
```json
|
||||
"customLocalFormatters.formatters": [
|
||||
{
|
||||
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
|
||||
"languages": ["qml"]
|
||||
}
|
||||
],
|
||||
"[qml]": {
|
||||
"editor.defaultFormatter": "jkillian.custom-local-formatters",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
```bash
|
||||
prek install
|
||||
```
|
||||
|
||||
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
|
||||
### Nix Development Shell
|
||||
|
||||
If you have Nix installed with flakes enabled, you can use the provided development shell which includes all necessary dependencies:
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
This will provide:
|
||||
|
||||
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||
- Quickshell and required QML packages
|
||||
- Properly configured QML2_IMPORT_PATH
|
||||
|
||||
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
||||
|
||||
## VSCode Setup
|
||||
|
||||
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
||||
|
||||
### QML (`quickshell` directory)
|
||||
|
||||
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
|
||||
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
|
||||
|
||||
```json
|
||||
{
|
||||
"qt-qml.doNotAskForQmllsDownload": true,
|
||||
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
|
||||
}
|
||||
```
|
||||
|
||||
3. Create empty `.qmlls.ini` file in `quickshell/` directory
|
||||
|
||||
```bash
|
||||
cd quickshell
|
||||
touch .qmlls.ini
|
||||
```
|
||||
|
||||
4. Restart dms to generate the `.qmlls.ini` file
|
||||
|
||||
5. Make your changes, test, and open a pull request.
|
||||
|
||||
### I18n/Localization
|
||||
|
||||
When adding user-facing strings, ensure they are wrapped in `I18n.tr()` with context, for example.
|
||||
|
||||
```qml
|
||||
import qs.Common
|
||||
|
||||
Text {
|
||||
text: I18n.tr("Hello World", "<This is context for the translators, example> Hello world greeting that appears on the lock screen")
|
||||
}
|
||||
```
|
||||
|
||||
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
||||
|
||||
### GO (`core` directory)
|
||||
|
||||
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||
2. Ensure code is formatted with `make fmt`
|
||||
3. Add appropriate test coverage and ensure tests pass with `make test`
|
||||
4. Run `go mod tidy`
|
||||
5. Open pull request
|
||||
|
||||
## Pull request
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: modalManager
|
||||
|
||||
signal closeAllModalsExcept(var excludedModal)
|
||||
|
||||
function openModal(modal) {
|
||||
if (!modal.allowStacking) {
|
||||
closeAllModalsExcept(modal)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtCore
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property url home: StandardPaths.standardLocations(
|
||||
StandardPaths.HomeLocation)[0]
|
||||
readonly property url pictures: StandardPaths.standardLocations(
|
||||
StandardPaths.PicturesLocation)[0]
|
||||
|
||||
readonly property url data: `${StandardPaths.standardLocations(
|
||||
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||
readonly property url state: `${StandardPaths.standardLocations(
|
||||
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||
readonly property url cache: `${StandardPaths.standardLocations(
|
||||
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
|
||||
readonly property url config: `${StandardPaths.standardLocations(
|
||||
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
|
||||
|
||||
readonly property url imagecache: `${cache}/imagecache`
|
||||
|
||||
function stringify(path: url): string {
|
||||
return path.toString().replace(/%20/g, " ")
|
||||
}
|
||||
|
||||
function expandTilde(path: string): string {
|
||||
return strip(path.replace("~", stringify(root.home)))
|
||||
}
|
||||
|
||||
function shortenHome(path: string): string {
|
||||
return path.replace(strip(root.home), "~")
|
||||
}
|
||||
|
||||
function strip(path: url): string {
|
||||
return stringify(path).replace("file://", "")
|
||||
}
|
||||
|
||||
function toFileUrl(path: string): string {
|
||||
return path.startsWith("file://") ? path : "file://" + path
|
||||
}
|
||||
|
||||
function mkdir(path: url): void {
|
||||
Quickshell.execDetached(["mkdir", "-p", strip(path)])
|
||||
}
|
||||
|
||||
function copy(from: url, to: url): void {
|
||||
Quickshell.execDetached(["cp", strip(from), strip(to)])
|
||||
}
|
||||
|
||||
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
|
||||
function moddedAppId(appId: string): string {
|
||||
if (appId === "Spotify")
|
||||
return "spotify"
|
||||
if (appId === "beepertexts")
|
||||
return "beeper"
|
||||
if (appId === "home assistant desktop")
|
||||
return "homeassistant-desktop"
|
||||
if (appId.includes("com.transmissionbt.transmission"))
|
||||
return "transmission-gtk"
|
||||
return appId
|
||||
}
|
||||
}
|
||||
110
Common/Proc.qml
110
Common/Proc.qml
@@ -1,110 +0,0 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int defaultDebounceMs: 50
|
||||
property int defaultTimeoutMs: 10000
|
||||
property var _procDebouncers: ({})
|
||||
|
||||
function runCommand(id, command, callback, debounceMs, timeoutMs) {
|
||||
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
|
||||
const timeout = (typeof timeoutMs === "number" && timeoutMs > 0) ? timeoutMs : defaultTimeoutMs
|
||||
let procId = id ? id : Math.random()
|
||||
const isRandomId = !id
|
||||
|
||||
if (!_procDebouncers[procId]) {
|
||||
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
|
||||
t.triggered.connect(function() { _launchProc(procId, isRandomId) })
|
||||
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait, timeoutMs: timeout, isRandomId: isRandomId }
|
||||
} else {
|
||||
_procDebouncers[procId].command = command
|
||||
_procDebouncers[procId].callback = callback
|
||||
_procDebouncers[procId].waitMs = wait
|
||||
_procDebouncers[procId].timeoutMs = timeout
|
||||
}
|
||||
|
||||
const entry = _procDebouncers[procId]
|
||||
entry.timer.interval = entry.waitMs
|
||||
entry.timer.restart()
|
||||
}
|
||||
|
||||
function _launchProc(id, isRandomId) {
|
||||
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)
|
||||
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
|
||||
|
||||
proc.stdout = out
|
||||
proc.stderr = err
|
||||
proc.command = entry.command
|
||||
|
||||
let capturedOut = ""
|
||||
let capturedErr = ""
|
||||
let exitSeen = false
|
||||
let exitCodeValue = -1
|
||||
let outSeen = false
|
||||
let errSeen = false
|
||||
let timedOut = false
|
||||
|
||||
timeoutTimer.interval = entry.timeoutMs
|
||||
timeoutTimer.triggered.connect(function() {
|
||||
if (!exitSeen) {
|
||||
timedOut = true
|
||||
proc.running = false
|
||||
exitSeen = true
|
||||
exitCodeValue = 124
|
||||
maybeComplete()
|
||||
}
|
||||
})
|
||||
|
||||
out.streamFinished.connect(function() {
|
||||
capturedOut = out.text || ""
|
||||
outSeen = true
|
||||
maybeComplete()
|
||||
})
|
||||
|
||||
err.streamFinished.connect(function() {
|
||||
capturedErr = err.text || ""
|
||||
errSeen = true
|
||||
maybeComplete()
|
||||
})
|
||||
|
||||
proc.exited.connect(function(code) {
|
||||
timeoutTimer.stop()
|
||||
exitSeen = true
|
||||
exitCodeValue = code
|
||||
maybeComplete()
|
||||
})
|
||||
|
||||
function maybeComplete() {
|
||||
if (!exitSeen || !outSeen || !errSeen) return
|
||||
timeoutTimer.stop()
|
||||
if (typeof entry.callback === "function") {
|
||||
try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) }
|
||||
}
|
||||
try { proc.destroy() } catch (_) {}
|
||||
try { timeoutTimer.destroy() } catch (_) {}
|
||||
|
||||
if (isRandomId || entry.isRandomId) {
|
||||
Qt.callLater(function() {
|
||||
if (_procDebouncers[id]) {
|
||||
try { _procDebouncers[id].timer.destroy() } catch (_) {}
|
||||
delete _procDebouncers[id]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
proc.running = true
|
||||
timeoutTimer.start()
|
||||
}
|
||||
}
|
||||
@@ -1,975 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int sessionConfigVersion: 1
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
property bool hasTriedDefaultSession: false
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
property bool isLightMode: false
|
||||
property bool doNotDisturb: false
|
||||
property bool isSwitchingMode: false
|
||||
|
||||
property string wallpaperPath: ""
|
||||
property bool perMonitorWallpaper: false
|
||||
property var monitorWallpapers: ({})
|
||||
property bool perModeWallpaper: false
|
||||
property string wallpaperPathLight: ""
|
||||
property string wallpaperPathDark: ""
|
||||
property var monitorWallpapersLight: ({})
|
||||
property var monitorWallpapersDark: ({})
|
||||
property string wallpaperTransition: "fade"
|
||||
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
|
||||
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
|
||||
|
||||
property bool wallpaperCyclingEnabled: false
|
||||
property string wallpaperCyclingMode: "interval"
|
||||
property int wallpaperCyclingInterval: 300
|
||||
property string wallpaperCyclingTime: "06:00"
|
||||
property var monitorCyclingSettings: ({})
|
||||
|
||||
property bool nightModeEnabled: false
|
||||
property int nightModeTemperature: 4500
|
||||
property int nightModeHighTemperature: 6500
|
||||
property bool nightModeAutoEnabled: false
|
||||
property string nightModeAutoMode: "time"
|
||||
property int nightModeStartHour: 18
|
||||
property int nightModeStartMinute: 0
|
||||
property int nightModeEndHour: 6
|
||||
property int nightModeEndMinute: 0
|
||||
property real latitude: 0.0
|
||||
property real longitude: 0.0
|
||||
property bool nightModeUseIPLocation: false
|
||||
property string nightModeLocationProvider: ""
|
||||
|
||||
property var pinnedApps: []
|
||||
property var hiddenTrayIds: []
|
||||
property var recentColors: []
|
||||
property bool showThirdPartyPlugins: false
|
||||
property string launchPrefix: ""
|
||||
property string lastBrightnessDevice: ""
|
||||
property var brightnessExponentialDevices: ({})
|
||||
property var brightnessUserSetValues: ({})
|
||||
property var brightnessExponentValues: ({})
|
||||
|
||||
property int selectedGpuIndex: 0
|
||||
property bool nvidiaGpuTempEnabled: false
|
||||
property bool nonNvidiaGpuTempEnabled: false
|
||||
property var enabledGpuPciIds: []
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
loadSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
if (isGreeterMode) {
|
||||
parseSettings(greeterSessionFile.text())
|
||||
} else {
|
||||
parseSettings(settingsFile.text())
|
||||
}
|
||||
}
|
||||
|
||||
function parseSettings(content) {
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
var settings = JSON.parse(content)
|
||||
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
|
||||
|
||||
if (settings.wallpaperPath && settings.wallpaperPath.startsWith("we:")) {
|
||||
console.warn("WallpaperEngine wallpaper detected, resetting wallpaper")
|
||||
wallpaperPath = ""
|
||||
Quickshell.execDetached([
|
||||
"notify-send",
|
||||
"-u", "critical",
|
||||
"-a", "DMS",
|
||||
"-i", "dialog-warning",
|
||||
"WallpaperEngine Support Moved",
|
||||
"WallpaperEngine support has been moved to a plugin. Please enable the Linux Wallpaper Engine plugin in Settings → Plugins to continue using WallpaperEngine."
|
||||
])
|
||||
} else {
|
||||
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
|
||||
}
|
||||
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
|
||||
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
|
||||
perModeWallpaper = settings.perModeWallpaper !== undefined ? settings.perModeWallpaper : false
|
||||
wallpaperPathLight = settings.wallpaperPathLight !== undefined ? settings.wallpaperPathLight : ""
|
||||
wallpaperPathDark = settings.wallpaperPathDark !== undefined ? settings.wallpaperPathDark : ""
|
||||
monitorWallpapersLight = settings.monitorWallpapersLight !== undefined ? settings.monitorWallpapersLight : {}
|
||||
monitorWallpapersDark = settings.monitorWallpapersDark !== undefined ? settings.monitorWallpapersDark : {}
|
||||
brightnessExponentialDevices = settings.brightnessExponentialDevices !== undefined ? settings.brightnessExponentialDevices : (settings.brightnessLogarithmicDevices || {})
|
||||
brightnessUserSetValues = settings.brightnessUserSetValues !== undefined ? settings.brightnessUserSetValues : {}
|
||||
brightnessExponentValues = settings.brightnessExponentValues !== undefined ? settings.brightnessExponentValues : {}
|
||||
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
|
||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
|
||||
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
|
||||
nightModeHighTemperature = settings.nightModeHighTemperature !== undefined ? settings.nightModeHighTemperature : 6500
|
||||
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
|
||||
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
|
||||
if (settings.nightModeStartTime !== undefined) {
|
||||
const parts = settings.nightModeStartTime.split(":")
|
||||
nightModeStartHour = parseInt(parts[0]) || 18
|
||||
nightModeStartMinute = parseInt(parts[1]) || 0
|
||||
} else {
|
||||
nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18
|
||||
nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0
|
||||
}
|
||||
if (settings.nightModeEndTime !== undefined) {
|
||||
const parts = settings.nightModeEndTime.split(":")
|
||||
nightModeEndHour = parseInt(parts[0]) || 6
|
||||
nightModeEndMinute = parseInt(parts[1]) || 0
|
||||
} else {
|
||||
nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6
|
||||
nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0
|
||||
}
|
||||
latitude = settings.latitude !== undefined ? settings.latitude : 0.0
|
||||
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
|
||||
nightModeUseIPLocation = settings.nightModeUseIPLocation !== undefined ? settings.nightModeUseIPLocation : false
|
||||
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
|
||||
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
|
||||
hiddenTrayIds = settings.hiddenTrayIds !== undefined ? settings.hiddenTrayIds : []
|
||||
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
||||
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
|
||||
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
||||
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
|
||||
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
|
||||
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
|
||||
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
|
||||
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
|
||||
monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {}
|
||||
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
|
||||
launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""
|
||||
wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"
|
||||
includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none")
|
||||
recentColors = settings.recentColors !== undefined ? settings.recentColors : []
|
||||
showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false
|
||||
|
||||
if (settings.configVersion === undefined) {
|
||||
migrateFromUndefinedToV1(settings)
|
||||
saveSettings()
|
||||
} else if (settings.configVersion === sessionConfigVersion) {
|
||||
cleanupUnusedKeys()
|
||||
}
|
||||
|
||||
if (!isGreeterMode) {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof WallpaperCyclingService !== "undefined") {
|
||||
WallpaperCyclingService.updateCyclingState()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (isGreeterMode)
|
||||
return
|
||||
settingsFile.setText(JSON.stringify({
|
||||
"isLightMode": isLightMode,
|
||||
"wallpaperPath": wallpaperPath,
|
||||
"perMonitorWallpaper": perMonitorWallpaper,
|
||||
"monitorWallpapers": monitorWallpapers,
|
||||
"perModeWallpaper": perModeWallpaper,
|
||||
"wallpaperPathLight": wallpaperPathLight,
|
||||
"wallpaperPathDark": wallpaperPathDark,
|
||||
"monitorWallpapersLight": monitorWallpapersLight,
|
||||
"monitorWallpapersDark": monitorWallpapersDark,
|
||||
"brightnessExponentialDevices": brightnessExponentialDevices,
|
||||
"brightnessUserSetValues": brightnessUserSetValues,
|
||||
"brightnessExponentValues": brightnessExponentValues,
|
||||
"doNotDisturb": doNotDisturb,
|
||||
"nightModeEnabled": nightModeEnabled,
|
||||
"nightModeTemperature": nightModeTemperature,
|
||||
"nightModeHighTemperature": nightModeHighTemperature,
|
||||
"nightModeAutoEnabled": nightModeAutoEnabled,
|
||||
"nightModeAutoMode": nightModeAutoMode,
|
||||
"nightModeStartHour": nightModeStartHour,
|
||||
"nightModeStartMinute": nightModeStartMinute,
|
||||
"nightModeEndHour": nightModeEndHour,
|
||||
"nightModeEndMinute": nightModeEndMinute,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"nightModeUseIPLocation": nightModeUseIPLocation,
|
||||
"nightModeLocationProvider": nightModeLocationProvider,
|
||||
"pinnedApps": pinnedApps,
|
||||
"hiddenTrayIds": hiddenTrayIds,
|
||||
"selectedGpuIndex": selectedGpuIndex,
|
||||
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
|
||||
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
|
||||
"enabledGpuPciIds": enabledGpuPciIds,
|
||||
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
|
||||
"wallpaperCyclingMode": wallpaperCyclingMode,
|
||||
"wallpaperCyclingInterval": wallpaperCyclingInterval,
|
||||
"wallpaperCyclingTime": wallpaperCyclingTime,
|
||||
"monitorCyclingSettings": monitorCyclingSettings,
|
||||
"lastBrightnessDevice": lastBrightnessDevice,
|
||||
"launchPrefix": launchPrefix,
|
||||
"wallpaperTransition": wallpaperTransition,
|
||||
"includedTransitions": includedTransitions,
|
||||
"recentColors": recentColors,
|
||||
"showThirdPartyPlugins": showThirdPartyPlugins,
|
||||
"configVersion": sessionConfigVersion
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(settings) {
|
||||
console.info("SessionData: Migrating configuration from undefined to version 1")
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
if (settings.acMonitorTimeout !== undefined) {
|
||||
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout)
|
||||
}
|
||||
if (settings.acLockTimeout !== undefined) {
|
||||
SettingsData.set("acLockTimeout", settings.acLockTimeout)
|
||||
}
|
||||
if (settings.acSuspendTimeout !== undefined) {
|
||||
SettingsData.set("acSuspendTimeout", settings.acSuspendTimeout)
|
||||
}
|
||||
if (settings.acHibernateTimeout !== undefined) {
|
||||
SettingsData.set("acHibernateTimeout", settings.acHibernateTimeout)
|
||||
}
|
||||
if (settings.batteryMonitorTimeout !== undefined) {
|
||||
SettingsData.set("batteryMonitorTimeout", settings.batteryMonitorTimeout)
|
||||
}
|
||||
if (settings.batteryLockTimeout !== undefined) {
|
||||
SettingsData.set("batteryLockTimeout", settings.batteryLockTimeout)
|
||||
}
|
||||
if (settings.batterySuspendTimeout !== undefined) {
|
||||
SettingsData.set("batterySuspendTimeout", settings.batterySuspendTimeout)
|
||||
}
|
||||
if (settings.batteryHibernateTimeout !== undefined) {
|
||||
SettingsData.set("batteryHibernateTimeout", settings.batteryHibernateTimeout)
|
||||
}
|
||||
if (settings.lockBeforeSuspend !== undefined) {
|
||||
SettingsData.set("lockBeforeSuspend", settings.lockBeforeSuspend)
|
||||
}
|
||||
if (settings.loginctlLockIntegration !== undefined) {
|
||||
SettingsData.set("loginctlLockIntegration", settings.loginctlLockIntegration)
|
||||
}
|
||||
if (settings.launchPrefix !== undefined) {
|
||||
SettingsData.set("launchPrefix", 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", "nightModeHighTemperature", "nightModeAutoEnabled", "nightModeAutoMode", "nightModeStartHour", "nightModeStartMinute", "nightModeEndHour", "nightModeEndMinute", "latitude", "longitude", "nightModeUseIPLocation", "nightModeLocationProvider", "pinnedApps", "hiddenTrayIds", "selectedGpuIndex", "nvidiaGpuTempEnabled", "nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled", "wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime", "monitorCyclingSettings", "lastBrightnessDevice", "brightnessExponentialDevices", "brightnessUserSetValues", "brightnessExponentValues", "launchPrefix", "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) {
|
||||
isSwitchingMode = true
|
||||
isLightMode = lightMode
|
||||
syncWallpaperForCurrentMode()
|
||||
saveSettings()
|
||||
Qt.callLater(() => { isSwitchingMode = false })
|
||||
}
|
||||
|
||||
function setDoNotDisturb(enabled) {
|
||||
doNotDisturb = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperPath(path) {
|
||||
wallpaperPath = path
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaper(imagePath) {
|
||||
wallpaperPath = imagePath
|
||||
if (perModeWallpaper) {
|
||||
if (isLightMode) {
|
||||
wallpaperPathLight = imagePath
|
||||
} else {
|
||||
wallpaperPathDark = imagePath
|
||||
}
|
||||
}
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setWallpaperColor(color) {
|
||||
wallpaperPath = color
|
||||
if (perModeWallpaper) {
|
||||
if (isLightMode) {
|
||||
wallpaperPathLight = color
|
||||
} else {
|
||||
wallpaperPathDark = color
|
||||
}
|
||||
}
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function clearWallpaper() {
|
||||
wallpaperPath = ""
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
|
||||
Theme.switchTheme(SettingsData.theme)
|
||||
} else {
|
||||
Theme.switchTheme("blue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setPerMonitorWallpaper(enabled) {
|
||||
perMonitorWallpaper = enabled
|
||||
if (enabled && perModeWallpaper) {
|
||||
syncWallpaperForCurrentMode()
|
||||
}
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setPerModeWallpaper(enabled) {
|
||||
if (enabled && wallpaperCyclingEnabled) {
|
||||
setWallpaperCyclingEnabled(false)
|
||||
}
|
||||
if (enabled && perMonitorWallpaper) {
|
||||
var monitorCyclingAny = false
|
||||
for (var key in monitorCyclingSettings) {
|
||||
if (monitorCyclingSettings[key].enabled) {
|
||||
monitorCyclingAny = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (monitorCyclingAny) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
for (var screenName in newSettings) {
|
||||
newSettings[screenName].enabled = false
|
||||
}
|
||||
monitorCyclingSettings = newSettings
|
||||
}
|
||||
}
|
||||
|
||||
perModeWallpaper = enabled
|
||||
if (enabled) {
|
||||
if (perMonitorWallpaper) {
|
||||
monitorWallpapersLight = Object.assign({}, monitorWallpapers)
|
||||
monitorWallpapersDark = Object.assign({}, monitorWallpapers)
|
||||
} else {
|
||||
wallpaperPathLight = wallpaperPath
|
||||
wallpaperPathDark = wallpaperPath
|
||||
}
|
||||
} else {
|
||||
syncWallpaperForCurrentMode()
|
||||
}
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setMonitorWallpaper(screenName, path) {
|
||||
var newMonitorWallpapers = Object.assign({}, monitorWallpapers)
|
||||
if (path && path !== "") {
|
||||
newMonitorWallpapers[screenName] = path
|
||||
} else {
|
||||
delete newMonitorWallpapers[screenName]
|
||||
}
|
||||
monitorWallpapers = newMonitorWallpapers
|
||||
|
||||
if (perModeWallpaper) {
|
||||
if (isLightMode) {
|
||||
var newLight = Object.assign({}, monitorWallpapersLight)
|
||||
if (path && path !== "") {
|
||||
newLight[screenName] = path
|
||||
} else {
|
||||
delete newLight[screenName]
|
||||
}
|
||||
monitorWallpapersLight = newLight
|
||||
} else {
|
||||
var newDark = Object.assign({}, monitorWallpapersDark)
|
||||
if (path && path !== "") {
|
||||
newDark[screenName] = path
|
||||
} else {
|
||||
delete newDark[screenName]
|
||||
}
|
||||
monitorWallpapersDark = newDark
|
||||
}
|
||||
}
|
||||
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined" && typeof SettingsData !== "undefined") {
|
||||
var screens = Quickshell.screens
|
||||
if (screens.length > 0) {
|
||||
var targetMonitor = (SettingsData.matugenTargetMonitor && SettingsData.matugenTargetMonitor !== "") ? SettingsData.matugenTargetMonitor : screens[0].name
|
||||
if (screenName === targetMonitor) {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setWallpaperTransition(transition) {
|
||||
wallpaperTransition = transition
|
||||
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 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 setNightModeHighTemperature(temperature) {
|
||||
nightModeHighTemperature = 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 setNightModeUseIPLocation(use) {
|
||||
nightModeUseIPLocation = use
|
||||
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 hideTrayId(trayId) {
|
||||
if (!trayId) return
|
||||
const current = [...hiddenTrayIds]
|
||||
if (current.indexOf(trayId) === -1) {
|
||||
current.push(trayId)
|
||||
hiddenTrayIds = current
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function showTrayId(trayId) {
|
||||
if (!trayId) return
|
||||
hiddenTrayIds = hiddenTrayIds.filter(id => id !== trayId)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function isHiddenTrayId(trayId) {
|
||||
return trayId && hiddenTrayIds.indexOf(trayId) !== -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()
|
||||
}
|
||||
|
||||
function setLaunchPrefix(prefix) {
|
||||
launchPrefix = prefix
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setLastBrightnessDevice(device) {
|
||||
lastBrightnessDevice = device
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setBrightnessExponential(deviceName, enabled) {
|
||||
var newSettings = Object.assign({}, brightnessExponentialDevices)
|
||||
if (enabled) {
|
||||
newSettings[deviceName] = true
|
||||
} else {
|
||||
delete newSettings[deviceName]
|
||||
}
|
||||
brightnessExponentialDevices = newSettings
|
||||
saveSettings()
|
||||
|
||||
if (typeof DisplayService !== "undefined") {
|
||||
DisplayService.updateDeviceBrightnessDisplay(deviceName)
|
||||
}
|
||||
}
|
||||
|
||||
function getBrightnessExponential(deviceName) {
|
||||
return brightnessExponentialDevices[deviceName] === true
|
||||
}
|
||||
|
||||
function setBrightnessUserSetValue(deviceName, value) {
|
||||
var newValues = Object.assign({}, brightnessUserSetValues)
|
||||
newValues[deviceName] = value
|
||||
brightnessUserSetValues = newValues
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function getBrightnessUserSetValue(deviceName) {
|
||||
return brightnessUserSetValues[deviceName]
|
||||
}
|
||||
|
||||
function setBrightnessExponent(deviceName, exponent) {
|
||||
var newValues = Object.assign({}, brightnessExponentValues)
|
||||
if (exponent !== undefined && exponent !== null) {
|
||||
newValues[deviceName] = exponent
|
||||
} else {
|
||||
delete newValues[deviceName]
|
||||
}
|
||||
brightnessExponentValues = newValues
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function getBrightnessExponent(deviceName) {
|
||||
const value = brightnessExponentValues[deviceName]
|
||||
return value !== undefined ? value : 1.2
|
||||
}
|
||||
|
||||
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 syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper)
|
||||
return
|
||||
|
||||
if (perMonitorWallpaper) {
|
||||
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
|
||||
return
|
||||
}
|
||||
|
||||
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark
|
||||
}
|
||||
|
||||
function getMonitorWallpaper(screenName) {
|
||||
if (!perMonitorWallpaper) {
|
||||
return wallpaperPath
|
||||
}
|
||||
return monitorWallpapers[screenName] || wallpaperPath
|
||||
}
|
||||
|
||||
function getMonitorCyclingSettings(screenName) {
|
||||
return monitorCyclingSettings[screenName] || {
|
||||
"enabled": false,
|
||||
"mode": "interval",
|
||||
"interval": 300,
|
||||
"time": "06:00"
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: settingsFile
|
||||
|
||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||
blockLoading: isGreeterMode
|
||||
blockWrites: true
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
if (!isGreeterMode) {
|
||||
parseSettings(settingsFile.text())
|
||||
hasTriedDefaultSession = false
|
||||
}
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!isGreeterMode && !hasTriedDefaultSession) {
|
||||
hasTriedDefaultSession = true
|
||||
defaultSessionCheckProcess.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: greeterSessionFile
|
||||
|
||||
path: {
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
||||
return greetCfgDir + "/session.json"
|
||||
}
|
||||
preload: isGreeterMode
|
||||
blockLoading: false
|
||||
blockWrites: true
|
||||
watchChanges: false
|
||||
printErrors: true
|
||||
onLoaded: {
|
||||
if (isGreeterMode) {
|
||||
parseSettings(greeterSessionFile.text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: defaultSessionCheckProcess
|
||||
|
||||
command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir
|
||||
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp --no-preserve=mode \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
console.info("Copied default-session.json to session.json")
|
||||
settingsFile.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "wallpaper"
|
||||
|
||||
function get(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead."
|
||||
}
|
||||
return root.wallpaperPath || ""
|
||||
}
|
||||
|
||||
function set(path: string): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead."
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return "ERROR: No path provided"
|
||||
}
|
||||
|
||||
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||
|
||||
try {
|
||||
root.setWallpaper(absolutePath)
|
||||
return "SUCCESS: Wallpaper set to " + absolutePath
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to set wallpaper: " + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function clear(): string {
|
||||
root.setWallpaper("")
|
||||
root.setPerMonitorWallpaper(false)
|
||||
root.monitorWallpapers = {}
|
||||
root.saveSettings()
|
||||
return "SUCCESS: All wallpapers cleared"
|
||||
}
|
||||
|
||||
function next(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead."
|
||||
}
|
||||
|
||||
if (!root.wallpaperPath) {
|
||||
return "ERROR: No wallpaper set"
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cycleNextManually()
|
||||
return "SUCCESS: Cycling to next wallpaper"
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function prev(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead."
|
||||
}
|
||||
|
||||
if (!root.wallpaperPath) {
|
||||
return "ERROR: No wallpaper set"
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cyclePrevManually()
|
||||
return "SUCCESS: Cycling to previous wallpaper"
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function getFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided"
|
||||
}
|
||||
return root.getMonitorWallpaper(screenName) || ""
|
||||
}
|
||||
|
||||
function setFor(screenName: string, path: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided"
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return "ERROR: No path provided"
|
||||
}
|
||||
|
||||
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||
|
||||
try {
|
||||
if (!root.perMonitorWallpaper) {
|
||||
root.setPerMonitorWallpaper(true)
|
||||
}
|
||||
root.setMonitorWallpaper(screenName, absolutePath)
|
||||
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function nextFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided"
|
||||
}
|
||||
|
||||
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||
if (!currentWallpaper) {
|
||||
return "ERROR: No wallpaper set for " + screenName
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cycleNextForMonitor(screenName)
|
||||
return "SUCCESS: Cycling to next wallpaper for " + screenName
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function prevFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided"
|
||||
}
|
||||
|
||||
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||
if (!currentWallpaper) {
|
||||
return "ERROR: No wallpaper set for " + screenName
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cyclePrevForMonitor(screenName)
|
||||
return "SUCCESS: Cycling to previous wallpaper for " + screenName
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,943 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Common.settings
|
||||
import qs.Services
|
||||
import "settings/SettingsSpec.js" as Spec
|
||||
import "settings/SettingsStore.js" as Store
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int settingsConfigVersion: 1
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
enum Position {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
enum AnimationSpeed {
|
||||
None,
|
||||
Short,
|
||||
Medium,
|
||||
Long,
|
||||
Custom
|
||||
}
|
||||
|
||||
enum SuspendBehavior {
|
||||
Suspend,
|
||||
Hibernate,
|
||||
SuspendThenHibernate
|
||||
}
|
||||
|
||||
readonly property string defaultFontFamily: "Inter Variable"
|
||||
readonly property string defaultMonoFontFamily: "Fira Code"
|
||||
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||
readonly property string _configDir: Paths.strip(_configUrl)
|
||||
readonly property string pluginSettingsPath: _configDir + "/DankMaterialShell/plugin_settings.json"
|
||||
|
||||
property bool _loading: false
|
||||
property bool _pluginSettingsLoading: false
|
||||
property bool hasTriedDefaultSettings: false
|
||||
property var pluginSettings: ({})
|
||||
|
||||
property alias dankBarLeftWidgetsModel: leftWidgetsModel
|
||||
property alias dankBarCenterWidgetsModel: centerWidgetsModel
|
||||
property alias dankBarRightWidgetsModel: rightWidgetsModel
|
||||
|
||||
property string currentThemeName: "blue"
|
||||
property string customThemeFile: ""
|
||||
property string matugenScheme: "scheme-tonal-spot"
|
||||
property bool runUserMatugenTemplates: true
|
||||
property string matugenTargetMonitor: ""
|
||||
property real dankBarTransparency: 1.0
|
||||
property real dankBarWidgetTransparency: 1.0
|
||||
property real popupTransparency: 1.0
|
||||
property real dockTransparency: 1
|
||||
property string widgetBackgroundColor: "sch"
|
||||
property real cornerRadius: 12
|
||||
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: false
|
||||
property bool useFahrenheit: false
|
||||
property bool nightModeEnabled: false
|
||||
property int animationSpeed: SettingsData.AnimationSpeed.Short
|
||||
property int customAnimationDuration: 500
|
||||
property string wallpaperFillMode: "Fill"
|
||||
property bool blurredWallpaperLayer: false
|
||||
property bool blurWallpaperOnOverview: false
|
||||
|
||||
property bool showLauncherButton: true
|
||||
property bool showWorkspaceSwitcher: true
|
||||
property bool showFocusedWindow: true
|
||||
property bool showWeather: true
|
||||
property bool showMusic: true
|
||||
property bool showClipboard: true
|
||||
property bool showCpuUsage: true
|
||||
property bool showMemUsage: true
|
||||
property bool showCpuTemp: true
|
||||
property bool showGpuTemp: true
|
||||
property int selectedGpuIndex: 0
|
||||
property var enabledGpuPciIds: []
|
||||
property bool showSystemTray: true
|
||||
property bool showClock: true
|
||||
property bool showNotificationButton: true
|
||||
property bool showBattery: true
|
||||
property bool showControlCenterButton: true
|
||||
|
||||
property bool controlCenterShowNetworkIcon: true
|
||||
property bool controlCenterShowBluetoothIcon: true
|
||||
property bool controlCenterShowAudioIcon: true
|
||||
property var controlCenterWidgets: [{
|
||||
"id": "volumeSlider",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "brightnessSlider",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "wifi",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "bluetooth",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "audioOutput",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "audioInput",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "nightMode",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "darkMode",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}]
|
||||
|
||||
property bool showWorkspaceIndex: false
|
||||
property bool showWorkspacePadding: false
|
||||
property bool workspaceScrolling: false
|
||||
property bool showWorkspaceApps: false
|
||||
property int maxWorkspaceIcons: 3
|
||||
property bool workspacesPerMonitor: true
|
||||
property bool dwlShowAllTags: false
|
||||
property var workspaceNameIcons: ({})
|
||||
property bool waveProgressEnabled: true
|
||||
property bool clockCompactMode: false
|
||||
property bool focusedWindowCompactMode: false
|
||||
property bool runningAppsCompactMode: true
|
||||
property bool keyboardLayoutNameCompactMode: false
|
||||
property bool runningAppsCurrentWorkspace: false
|
||||
property bool runningAppsGroupByApp: false
|
||||
property string clockDateFormat: ""
|
||||
property string lockDateFormat: ""
|
||||
property int mediaSize: 1
|
||||
|
||||
property var dankBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
||||
property var dankBarCenterWidgets: ["music", "clock", "weather"]
|
||||
property var dankBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
|
||||
property var dankBarWidgetOrder: []
|
||||
|
||||
property string appLauncherViewMode: "list"
|
||||
property string spotlightModalViewMode: "list"
|
||||
property bool sortAppsAlphabetically: false
|
||||
|
||||
property string weatherLocation: "New York, NY"
|
||||
property string weatherCoordinates: "40.7128,-74.0060"
|
||||
property bool useAutoLocation: false
|
||||
property bool weatherEnabled: true
|
||||
|
||||
property string networkPreference: "auto"
|
||||
property string vpnLastConnected: ""
|
||||
|
||||
property string iconTheme: "System Default"
|
||||
property var availableIconThemes: ["System Default"]
|
||||
property string systemDefaultIconTheme: ""
|
||||
property bool qt5ctAvailable: false
|
||||
property bool qt6ctAvailable: false
|
||||
property bool gtkAvailable: false
|
||||
|
||||
property string launcherLogoMode: "apps"
|
||||
property string launcherLogoCustomPath: ""
|
||||
property string launcherLogoColorOverride: ""
|
||||
property bool launcherLogoColorInvertOnMode: false
|
||||
property real launcherLogoBrightness: 0.5
|
||||
property real launcherLogoContrast: 1
|
||||
property int launcherLogoSizeOffset: 0
|
||||
|
||||
property string fontFamily: "Inter Variable"
|
||||
property string monoFontFamily: "Fira Code"
|
||||
property int fontWeight: Font.Normal
|
||||
property real fontScale: 1.0
|
||||
property real dankBarFontScale: 1.0
|
||||
|
||||
property bool notepadUseMonospace: true
|
||||
property string notepadFontFamily: ""
|
||||
property real notepadFontSize: 14
|
||||
property bool notepadShowLineNumbers: false
|
||||
property real notepadTransparencyOverride: -1
|
||||
property real notepadLastCustomTransparency: 0.7
|
||||
|
||||
onNotepadUseMonospaceChanged: saveSettings()
|
||||
onNotepadFontFamilyChanged: saveSettings()
|
||||
onNotepadFontSizeChanged: saveSettings()
|
||||
onNotepadShowLineNumbersChanged: saveSettings()
|
||||
onNotepadTransparencyOverrideChanged: {
|
||||
if (notepadTransparencyOverride > 0) {
|
||||
notepadLastCustomTransparency = notepadTransparencyOverride
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
onNotepadLastCustomTransparencyChanged: saveSettings()
|
||||
|
||||
property bool soundsEnabled: true
|
||||
property bool useSystemSoundTheme: false
|
||||
property bool soundNewNotification: true
|
||||
property bool soundVolumeChanged: true
|
||||
property bool soundPluggedIn: true
|
||||
|
||||
property int acMonitorTimeout: 0
|
||||
property int acLockTimeout: 0
|
||||
property int acSuspendTimeout: 0
|
||||
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||
property int batteryMonitorTimeout: 0
|
||||
property int batteryLockTimeout: 0
|
||||
property int batterySuspendTimeout: 0
|
||||
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||
property bool lockBeforeSuspend: false
|
||||
property bool preventIdleForMedia: false
|
||||
property bool loginctlLockIntegration: true
|
||||
property string launchPrefix: ""
|
||||
property var brightnessDevicePins: ({})
|
||||
|
||||
property bool gtkThemingEnabled: false
|
||||
property bool qtThemingEnabled: false
|
||||
property bool syncModeWithPortal: true
|
||||
|
||||
property bool showDock: false
|
||||
property bool dockAutoHide: false
|
||||
property bool dockGroupByApp: false
|
||||
property bool dockOpenOnOverview: false
|
||||
property int dockPosition: SettingsData.Position.Bottom
|
||||
property real dockSpacing: 4
|
||||
property real dockBottomGap: 0
|
||||
property real dockMargin: 0
|
||||
property real dockIconSize: 40
|
||||
property string dockIndicatorStyle: "circle"
|
||||
|
||||
property bool notificationOverlayEnabled: false
|
||||
property bool dankBarAutoHide: false
|
||||
property bool dankBarOpenOnOverview: false
|
||||
property bool dankBarVisible: true
|
||||
property int overviewRows: 2
|
||||
property int overviewColumns: 5
|
||||
property real overviewScale: 0.16
|
||||
property real dankBarSpacing: 4
|
||||
property real dankBarBottomGap: 0
|
||||
property real dankBarInnerPadding: 4
|
||||
property int dankBarPosition: SettingsData.Position.Top
|
||||
property bool dankBarIsVertical: dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right
|
||||
|
||||
property bool dankBarSquareCorners: false
|
||||
property bool dankBarNoBackground: false
|
||||
property bool dankBarGothCornersEnabled: false
|
||||
property bool dankBarGothCornerRadiusOverride: false
|
||||
property real dankBarGothCornerRadiusValue: 12
|
||||
property bool dankBarBorderEnabled: false
|
||||
property string dankBarBorderColor: "surfaceText"
|
||||
property real dankBarBorderOpacity: 1.0
|
||||
property real dankBarBorderThickness: 1
|
||||
|
||||
onDankBarGothCornerRadiusOverrideChanged: saveSettings()
|
||||
onDankBarGothCornerRadiusValueChanged: saveSettings()
|
||||
onDankBarBorderColorChanged: saveSettings()
|
||||
onDankBarBorderOpacityChanged: saveSettings()
|
||||
onDankBarBorderThicknessChanged: saveSettings()
|
||||
|
||||
property bool popupGapsAuto: true
|
||||
property int popupGapsManual: 4
|
||||
|
||||
property bool modalDarkenBackground: true
|
||||
|
||||
property bool lockScreenShowPowerActions: true
|
||||
property bool enableFprint: false
|
||||
property int maxFprintTries: 3
|
||||
property bool fprintdAvailable: false
|
||||
property bool hideBrightnessSlider: false
|
||||
|
||||
property int notificationTimeoutLow: 5000
|
||||
property int notificationTimeoutNormal: 5000
|
||||
property int notificationTimeoutCritical: 0
|
||||
property int notificationPopupPosition: SettingsData.Position.Top
|
||||
|
||||
property bool osdAlwaysShowValue: false
|
||||
|
||||
property bool powerActionConfirm: true
|
||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||
property string powerMenuDefaultAction: "logout"
|
||||
property string customPowerActionLock: ""
|
||||
property string customPowerActionLogout: ""
|
||||
property string customPowerActionSuspend: ""
|
||||
property string customPowerActionHibernate: ""
|
||||
property string customPowerActionReboot: ""
|
||||
property string customPowerActionPowerOff: ""
|
||||
|
||||
property bool updaterUseCustomCommand: false
|
||||
property string updaterCustomCommand: ""
|
||||
property string updaterTerminalAdditionalParams: ""
|
||||
|
||||
property var screenPreferences: ({})
|
||||
property var showOnLastDisplay: ({})
|
||||
|
||||
signal forceDankBarLayoutRefresh
|
||||
signal forceDockLayoutRefresh
|
||||
signal widgetDataChanged
|
||||
signal workspaceIconsUpdated
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
Processes.settingsRoot = root
|
||||
loadSettings()
|
||||
initializeListModels()
|
||||
Processes.detectFprintd()
|
||||
Processes.checkPluginSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function applyStoredTheme() {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.switchTheme(currentThemeName, false, false)
|
||||
} else {
|
||||
Qt.callLater(function() {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.switchTheme(currentThemeName, false, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function regenSystemThemes() {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function updateNiriLayout() {
|
||||
if (typeof NiriService !== "undefined" && typeof CompositorService !== "undefined" && CompositorService.isNiri) {
|
||||
NiriService.generateNiriLayoutConfig()
|
||||
}
|
||||
}
|
||||
|
||||
function applyStoredIconTheme() {
|
||||
updateGtkIconTheme()
|
||||
updateQtIconTheme()
|
||||
}
|
||||
|
||||
function updateGtkIconTheme() {
|
||||
const gtkThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme
|
||||
if (gtkThemeName === "System Default" || gtkThemeName === "") return
|
||||
|
||||
if (typeof DMSService !== "undefined" && DMSService.apiVersion >= 3 && typeof PortalService !== "undefined") {
|
||||
PortalService.setSystemIconTheme(gtkThemeName)
|
||||
}
|
||||
|
||||
const configScript = `mkdir -p ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0
|
||||
|
||||
for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do
|
||||
settings_file="$config_dir/settings.ini"
|
||||
if [ -f "$settings_file" ]; then
|
||||
if grep -q "^gtk-icon-theme-name=" "$settings_file"; then
|
||||
sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file"
|
||||
else
|
||||
if grep -q "\\[Settings\\]" "$settings_file"; then
|
||||
sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file"
|
||||
else
|
||||
echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e '[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' > "$settings_file"
|
||||
fi
|
||||
done
|
||||
|
||||
rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true
|
||||
pkill -HUP -f 'gtk' 2>/dev/null || true`
|
||||
|
||||
Quickshell.execDetached(["sh", "-lc", configScript])
|
||||
}
|
||||
|
||||
function updateQtIconTheme() {
|
||||
const qtThemeName = (iconTheme === "System Default") ? "" : iconTheme
|
||||
if (!qtThemeName) return
|
||||
|
||||
const home = _homeUrl.replace("file://", "").replace(/'/g, "'\\''")
|
||||
const qtThemeNameEscaped = qtThemeName.replace(/'/g, "'\\''")
|
||||
|
||||
const script = `mkdir -p ${_configDir}/qt5ct ${_configDir}/qt6ct ${_configDir}/environment.d 2>/dev/null || true
|
||||
update_qt_icon_theme() {
|
||||
local config_file="$1"
|
||||
local theme_name="$2"
|
||||
if [ -f "$config_file" ]; then
|
||||
if grep -q "^\\[Appearance\\]" "$config_file"; then
|
||||
if grep -q "^icon_theme=" "$config_file"; then
|
||||
sed -i "s/^icon_theme=.*/icon_theme=$theme_name/" "$config_file"
|
||||
else
|
||||
sed -i "/^\\[Appearance\\]/a icon_theme=$theme_name" "$config_file"
|
||||
fi
|
||||
else
|
||||
printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file"
|
||||
fi
|
||||
else
|
||||
printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file"
|
||||
fi
|
||||
}
|
||||
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
|
||||
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'
|
||||
rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`
|
||||
|
||||
Quickshell.execDetached(["sh", "-lc", script])
|
||||
}
|
||||
|
||||
readonly property var _hooks: ({
|
||||
applyStoredTheme: applyStoredTheme,
|
||||
regenSystemThemes: regenSystemThemes,
|
||||
updateNiriLayout: updateNiriLayout,
|
||||
applyStoredIconTheme: applyStoredIconTheme
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
Spec.set(root, key, value, saveSettings, _hooks)
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
_loading = true
|
||||
try {
|
||||
const txt = settingsFile.text()
|
||||
const obj = (txt && txt.trim()) ? JSON.parse(txt) : null
|
||||
Store.parse(root, obj)
|
||||
const shouldMigrate = Store.migrate(root, obj)
|
||||
applyStoredTheme()
|
||||
applyStoredIconTheme()
|
||||
Processes.detectIcons()
|
||||
Processes.detectQtTools()
|
||||
if (obj && obj.configVersion === undefined) {
|
||||
const cleaned = Store.cleanup(txt)
|
||||
if (cleaned) {
|
||||
settingsFile.setText(cleaned)
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
if (shouldMigrate) {
|
||||
savePluginSettings()
|
||||
saveSettings()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to load settings:", e.message)
|
||||
applyStoredTheme()
|
||||
applyStoredIconTheme()
|
||||
} finally {
|
||||
_loading = false
|
||||
}
|
||||
loadPluginSettings()
|
||||
}
|
||||
|
||||
function loadPluginSettings() {
|
||||
_pluginSettingsLoading = true
|
||||
parsePluginSettings(pluginSettingsFile.text())
|
||||
_pluginSettingsLoading = false
|
||||
}
|
||||
|
||||
function parsePluginSettings(content) {
|
||||
_pluginSettingsLoading = true
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
pluginSettings = JSON.parse(content)
|
||||
} else {
|
||||
pluginSettings = {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to parse plugin settings:", e.message)
|
||||
pluginSettings = {}
|
||||
} finally {
|
||||
_pluginSettingsLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (_loading) return
|
||||
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2))
|
||||
}
|
||||
|
||||
function savePluginSettings() {
|
||||
if (_pluginSettingsLoading) return
|
||||
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2))
|
||||
}
|
||||
|
||||
function detectAvailableIconThemes() {
|
||||
Processes.detectIcons()
|
||||
}
|
||||
|
||||
function getEffectiveTimeFormat() {
|
||||
if (use24HourClock) {
|
||||
return showSeconds ? "hh:mm:ss" : "hh:mm"
|
||||
} else {
|
||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP"
|
||||
}
|
||||
}
|
||||
|
||||
function getEffectiveClockDateFormat() {
|
||||
return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d"
|
||||
}
|
||||
|
||||
function getEffectiveLockDateFormat() {
|
||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat
|
||||
}
|
||||
|
||||
function initializeListModels() {
|
||||
Lists.init(leftWidgetsModel, centerWidgetsModel, rightWidgetsModel, dankBarLeftWidgets, dankBarCenterWidgets, dankBarRightWidgets)
|
||||
}
|
||||
|
||||
function updateListModel(listModel, order) {
|
||||
Lists.update(listModel, order)
|
||||
widgetDataChanged()
|
||||
}
|
||||
|
||||
function hasNamedWorkspaces() {
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri) return false
|
||||
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
var ws = NiriService.allWorkspaces[i]
|
||||
if (ws.name && ws.name.trim() !== "") return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getNamedWorkspaces() {
|
||||
var namedWorkspaces = []
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri) return namedWorkspaces
|
||||
|
||||
for (const ws of NiriService.allWorkspaces) {
|
||||
if (ws.name && ws.name.trim() !== "") {
|
||||
namedWorkspaces.push(ws.name)
|
||||
}
|
||||
}
|
||||
return namedWorkspaces
|
||||
}
|
||||
|
||||
function getPopupYPosition(barHeight) {
|
||||
const gothOffset = dankBarGothCornersEnabled ? Theme.cornerRadius : 0
|
||||
return barHeight + dankBarSpacing + dankBarBottomGap - gothOffset + Theme.popupDistance
|
||||
}
|
||||
|
||||
function getPopupTriggerPosition(globalPos, screen, barThickness, widgetWidth) {
|
||||
const screenX = screen ? screen.x : 0
|
||||
const screenY = screen ? screen.y : 0
|
||||
const relativeX = globalPos.x - screenX
|
||||
const relativeY = globalPos.y - screenY
|
||||
|
||||
if (dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right) {
|
||||
return {
|
||||
"x": relativeY,
|
||||
"y": barThickness + dankBarSpacing + Theme.popupDistance,
|
||||
"width": widgetWidth
|
||||
}
|
||||
}
|
||||
return {
|
||||
"x": relativeX,
|
||||
"y": barThickness + dankBarSpacing + Theme.popupDistance,
|
||||
"width": widgetWidth
|
||||
}
|
||||
}
|
||||
|
||||
function getFilteredScreens(componentId) {
|
||||
var prefs = screenPreferences && screenPreferences[componentId] || ["all"]
|
||||
if (prefs.includes("all")) {
|
||||
return Quickshell.screens
|
||||
}
|
||||
var filtered = Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
||||
if (filtered.length === 0 && showOnLastDisplay && showOnLastDisplay[componentId] && Quickshell.screens.length === 1) {
|
||||
return Quickshell.screens
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
function sendTestNotifications() {
|
||||
sendTestNotification(0)
|
||||
testNotifTimer1.start()
|
||||
testNotifTimer2.start()
|
||||
}
|
||||
|
||||
function sendTestNotification(index) {
|
||||
const notifications = [["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"], ["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"], ["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]]
|
||||
|
||||
if (index < 0 || index >= notifications.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const notif = notifications[index]
|
||||
testNotificationProcess.command = ["notify-send", "-h", "int:transient:1", "-a", "DMS", "-i", notif[2], notif[0], notif[1]]
|
||||
testNotificationProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
function setMatugenScheme(scheme) {
|
||||
var normalized = scheme || "scheme-tonal-spot"
|
||||
if (matugenScheme === normalized) return
|
||||
set("matugenScheme", normalized)
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setRunUserMatugenTemplates(enabled) {
|
||||
if (runUserMatugenTemplates === enabled) return
|
||||
set("runUserMatugenTemplates", enabled)
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setMatugenTargetMonitor(monitorName) {
|
||||
if (matugenTargetMonitor === monitorName) return
|
||||
set("matugenTargetMonitor", monitorName)
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setCornerRadius(radius) {
|
||||
set("cornerRadius", radius)
|
||||
NiriService.generateNiriLayoutConfig()
|
||||
}
|
||||
|
||||
function setWeatherLocation(displayName, coordinates) {
|
||||
weatherLocation = displayName
|
||||
weatherCoordinates = coordinates
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setIconTheme(themeName) {
|
||||
iconTheme = themeName
|
||||
updateGtkIconTheme()
|
||||
updateQtIconTheme()
|
||||
saveSettings()
|
||||
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
|
||||
function setGtkThemingEnabled(enabled) {
|
||||
set("gtkThemingEnabled", enabled)
|
||||
if (enabled && typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setQtThemingEnabled(enabled) {
|
||||
set("qtThemingEnabled", enabled)
|
||||
if (enabled && typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setShowDock(enabled) {
|
||||
showDock = enabled
|
||||
if (enabled && dockPosition === dankBarPosition) {
|
||||
if (dankBarPosition === SettingsData.Position.Top) {
|
||||
setDockPosition(SettingsData.Position.Bottom)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Bottom) {
|
||||
setDockPosition(SettingsData.Position.Top)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Left) {
|
||||
setDockPosition(SettingsData.Position.Right)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Right) {
|
||||
setDockPosition(SettingsData.Position.Left)
|
||||
return
|
||||
}
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDockPosition(position) {
|
||||
dockPosition = position
|
||||
if (position === SettingsData.Position.Bottom && dankBarPosition === SettingsData.Position.Bottom && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Top)
|
||||
}
|
||||
if (position === SettingsData.Position.Top && dankBarPosition === SettingsData.Position.Top && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Bottom)
|
||||
}
|
||||
if (position === SettingsData.Position.Left && dankBarPosition === SettingsData.Position.Left && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Right)
|
||||
}
|
||||
if (position === SettingsData.Position.Right && dankBarPosition === SettingsData.Position.Right && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Left)
|
||||
}
|
||||
saveSettings()
|
||||
Qt.callLater(() => forceDockLayoutRefresh())
|
||||
}
|
||||
|
||||
function setDankBarSpacing(spacing) {
|
||||
set("dankBarSpacing", spacing)
|
||||
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
||||
NiriService.generateNiriLayoutConfig()
|
||||
}
|
||||
}
|
||||
|
||||
function setDankBarPosition(position) {
|
||||
dankBarPosition = position
|
||||
if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) {
|
||||
setDockPosition(SettingsData.Position.Top)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Top && dockPosition === SettingsData.Position.Top && showDock) {
|
||||
setDockPosition(SettingsData.Position.Bottom)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Left && dockPosition === SettingsData.Position.Left && showDock) {
|
||||
setDockPosition(SettingsData.Position.Right)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Right && dockPosition === SettingsData.Position.Right && showDock) {
|
||||
setDockPosition(SettingsData.Position.Left)
|
||||
return
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDankBarLeftWidgets(order) {
|
||||
dankBarLeftWidgets = order
|
||||
updateListModel(leftWidgetsModel, order)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDankBarCenterWidgets(order) {
|
||||
dankBarCenterWidgets = order
|
||||
updateListModel(centerWidgetsModel, order)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDankBarRightWidgets(order) {
|
||||
dankBarRightWidgets = order
|
||||
updateListModel(rightWidgetsModel, order)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function resetDankBarWidgetsToDefault() {
|
||||
var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
||||
var defaultCenter = ["music", "clock", "weather"]
|
||||
var defaultRight = ["systemTray", "clipboard", "notificationButton", "battery", "controlCenterButton"]
|
||||
dankBarLeftWidgets = defaultLeft
|
||||
dankBarCenterWidgets = defaultCenter
|
||||
dankBarRightWidgets = defaultRight
|
||||
updateListModel(leftWidgetsModel, defaultLeft)
|
||||
updateListModel(centerWidgetsModel, defaultCenter)
|
||||
updateListModel(rightWidgetsModel, defaultRight)
|
||||
showLauncherButton = true
|
||||
showWorkspaceSwitcher = true
|
||||
showFocusedWindow = true
|
||||
showWeather = true
|
||||
showMusic = true
|
||||
showClipboard = true
|
||||
showCpuUsage = true
|
||||
showMemUsage = true
|
||||
showCpuTemp = true
|
||||
showGpuTemp = true
|
||||
showSystemTray = true
|
||||
showClock = true
|
||||
showNotificationButton = true
|
||||
showBattery = true
|
||||
showControlCenterButton = true
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWorkspaceNameIcon(workspaceName, iconData) {
|
||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
||||
iconMap[workspaceName] = iconData
|
||||
workspaceNameIcons = iconMap
|
||||
saveSettings()
|
||||
workspaceIconsUpdated()
|
||||
}
|
||||
|
||||
function removeWorkspaceNameIcon(workspaceName) {
|
||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
||||
delete iconMap[workspaceName]
|
||||
workspaceNameIcons = iconMap
|
||||
saveSettings()
|
||||
workspaceIconsUpdated()
|
||||
}
|
||||
|
||||
function getWorkspaceNameIcon(workspaceName) {
|
||||
return workspaceNameIcons[workspaceName] || null
|
||||
}
|
||||
|
||||
function toggleDankBarVisible() {
|
||||
dankBarVisible = !dankBarVisible
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function getPluginSetting(pluginId, key, defaultValue) {
|
||||
if (!pluginSettings[pluginId]) {
|
||||
return defaultValue
|
||||
}
|
||||
return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue
|
||||
}
|
||||
|
||||
function setPluginSetting(pluginId, key, value) {
|
||||
const updated = JSON.parse(JSON.stringify(pluginSettings))
|
||||
if (!updated[pluginId]) {
|
||||
updated[pluginId] = {}
|
||||
}
|
||||
updated[pluginId][key] = value
|
||||
pluginSettings = updated
|
||||
savePluginSettings()
|
||||
}
|
||||
|
||||
function removePluginSettings(pluginId) {
|
||||
if (pluginSettings[pluginId]) {
|
||||
delete pluginSettings[pluginId]
|
||||
savePluginSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginSettingsForPlugin(pluginId) {
|
||||
const settings = pluginSettings[pluginId]
|
||||
return settings ? JSON.parse(JSON.stringify(settings)) : {}
|
||||
}
|
||||
|
||||
|
||||
ListModel {
|
||||
id: leftWidgetsModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: centerWidgetsModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: rightWidgetsModel
|
||||
}
|
||||
|
||||
property Process testNotificationProcess
|
||||
|
||||
testNotificationProcess: Process {
|
||||
command: []
|
||||
running: false
|
||||
}
|
||||
|
||||
property Timer testNotifTimer1
|
||||
|
||||
testNotifTimer1: Timer {
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: sendTestNotification(1)
|
||||
}
|
||||
|
||||
property Timer testNotifTimer2
|
||||
|
||||
testNotifTimer2: Timer {
|
||||
interval: 800
|
||||
repeat: false
|
||||
onTriggered: sendTestNotification(2)
|
||||
}
|
||||
|
||||
property alias settingsFile: settingsFile
|
||||
|
||||
FileView {
|
||||
id: settingsFile
|
||||
|
||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
if (!isGreeterMode) {
|
||||
try {
|
||||
const txt = settingsFile.text()
|
||||
const obj = (txt && txt.trim()) ? JSON.parse(txt) : null
|
||||
Store.parse(root, obj)
|
||||
Store.migrate(root, obj)
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to reload settings:", e.message)
|
||||
}
|
||||
hasTriedDefaultSettings = false
|
||||
}
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!isGreeterMode && !hasTriedDefaultSettings) {
|
||||
hasTriedDefaultSettings = true
|
||||
Processes.checkDefaultSettings()
|
||||
} else if (!isGreeterMode) {
|
||||
applyStoredTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: pluginSettingsFile
|
||||
|
||||
path: isGreeterMode ? "" : pluginSettingsPath
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
if (!isGreeterMode) {
|
||||
parsePluginSettings(pluginSettingsFile.text())
|
||||
}
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!isGreeterMode) {
|
||||
pluginSettings = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool pluginSettingsFileExists: false
|
||||
|
||||
IpcHandler {
|
||||
function reveal(): string {
|
||||
root.setDankBarVisible(true)
|
||||
return "BAR_SHOW_SUCCESS"
|
||||
}
|
||||
|
||||
function hide(): string {
|
||||
root.setDankBarVisible(false)
|
||||
return "BAR_HIDE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
root.toggleDankBarVisible()
|
||||
return root.dankBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS"
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
return root.dankBarVisible ? "visible" : "hidden"
|
||||
}
|
||||
|
||||
target: "bar"
|
||||
}
|
||||
}
|
||||
@@ -1,545 +0,0 @@
|
||||
// Stock theme definitions for DankMaterialShell
|
||||
// Separated from Theme.qml to keep that file clean
|
||||
|
||||
const CatppuccinMocha = {
|
||||
surface: "#313244",
|
||||
surfaceText: "#cdd6f4",
|
||||
surfaceVariant: "#313244",
|
||||
surfaceVariantText: "#a6adc8",
|
||||
background: "#1e1e2e",
|
||||
backgroundText: "#cdd6f4",
|
||||
outline: "#6c7086",
|
||||
surfaceContainer: "#45475a",
|
||||
surfaceContainerHigh: "#585b70",
|
||||
surfaceContainerHighest: "#6c7086"
|
||||
}
|
||||
|
||||
const CatppuccinLatte = {
|
||||
surface: "#e6e9ef",
|
||||
surfaceText: "#4c4f69",
|
||||
surfaceVariant: "#e6e9ef",
|
||||
surfaceVariantText: "#6c6f85",
|
||||
background: "#eff1f5",
|
||||
backgroundText: "#4c4f69",
|
||||
outline: "#9ca0b0",
|
||||
surfaceContainer: "#dce0e8",
|
||||
surfaceContainerHigh: "#ccd0da",
|
||||
surfaceContainerHighest: "#bcc0cc"
|
||||
}
|
||||
|
||||
const CatppuccinVariants = {
|
||||
"cat-rosewater": {
|
||||
name: "Rosewater",
|
||||
dark: { primary: "#f5e0dc", secondary: "#f2cdcd", primaryText: "#1e1e2e", primaryContainer: "#7d5d56", surfaceTint: "#f5e0dc" },
|
||||
light: { primary: "#dc8a78", secondary: "#dd7878", primaryText: "#ffffff", primaryContainer: "#f6e7e3", surfaceTint: "#dc8a78" }
|
||||
},
|
||||
"cat-flamingo": {
|
||||
name: "Flamingo",
|
||||
dark: { primary: "#f2cdcd", secondary: "#f5e0dc", primaryText: "#1e1e2e", primaryContainer: "#7a555a", surfaceTint: "#f2cdcd" },
|
||||
light: { primary: "#dd7878", secondary: "#dc8a78", primaryText: "#ffffff", primaryContainer: "#f6e5e5", surfaceTint: "#dd7878" }
|
||||
},
|
||||
"cat-pink": {
|
||||
name: "Pink",
|
||||
dark: { primary: "#f5c2e7", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#7a3f69", surfaceTint: "#f5c2e7" },
|
||||
light: { primary: "#ea76cb", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#f7d7ee", surfaceTint: "#ea76cb" }
|
||||
},
|
||||
"cat-mauve": {
|
||||
name: "Mauve",
|
||||
dark: { primary: "#cba6f7", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#55307f", surfaceTint: "#cba6f7" },
|
||||
light: { primary: "#8839ef", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#eadcff", surfaceTint: "#8839ef" }
|
||||
},
|
||||
"cat-red": {
|
||||
name: "Red",
|
||||
dark: { primary: "#f38ba8", secondary: "#eba0ac", primaryText: "#1e1e2e", primaryContainer: "#6f2438", surfaceTint: "#f38ba8" },
|
||||
light: { primary: "#d20f39", secondary: "#e64553", primaryText: "#ffffff", primaryContainer: "#f6d0d6", surfaceTint: "#d20f39" }
|
||||
},
|
||||
"cat-maroon": {
|
||||
name: "Maroon",
|
||||
dark: { primary: "#eba0ac", secondary: "#f38ba8", primaryText: "#1e1e2e", primaryContainer: "#6d3641", surfaceTint: "#eba0ac" },
|
||||
light: { primary: "#e64553", secondary: "#d20f39", primaryText: "#ffffff", primaryContainer: "#f7d8dc", surfaceTint: "#e64553" }
|
||||
},
|
||||
"cat-peach": {
|
||||
name: "Peach",
|
||||
dark: { primary: "#fab387", secondary: "#f9e2af", primaryText: "#1e1e2e", primaryContainer: "#734226", surfaceTint: "#fab387" },
|
||||
light: { primary: "#fe640b", secondary: "#df8e1d", primaryText: "#ffffff", primaryContainer: "#ffe4d5", surfaceTint: "#fe640b" }
|
||||
},
|
||||
"cat-yellow": {
|
||||
name: "Yellow",
|
||||
dark: { primary: "#f9e2af", secondary: "#a6e3a1", primaryText: "#1e1e2e", primaryContainer: "#6e5a2f", surfaceTint: "#f9e2af" },
|
||||
light: { primary: "#df8e1d", secondary: "#40a02b", primaryText: "#ffffff", primaryContainer: "#fff6d6", surfaceTint: "#df8e1d" }
|
||||
},
|
||||
"cat-green": {
|
||||
name: "Green",
|
||||
dark: { primary: "#a6e3a1", secondary: "#94e2d5", primaryText: "#1e1e2e", primaryContainer: "#2f5f36", surfaceTint: "#a6e3a1" },
|
||||
light: { primary: "#40a02b", secondary: "#179299", primaryText: "#ffffff", primaryContainer: "#dff4e0", surfaceTint: "#40a02b" }
|
||||
},
|
||||
"cat-teal": {
|
||||
name: "Teal",
|
||||
dark: { primary: "#94e2d5", secondary: "#89dceb", primaryText: "#1e1e2e", primaryContainer: "#2e5e59", surfaceTint: "#94e2d5" },
|
||||
light: { primary: "#179299", secondary: "#04a5e5", primaryText: "#ffffff", primaryContainer: "#daf3f1", surfaceTint: "#179299" }
|
||||
},
|
||||
"cat-sky": {
|
||||
name: "Sky",
|
||||
dark: { primary: "#89dceb", secondary: "#74c7ec", primaryText: "#1e1e2e", primaryContainer: "#24586a", surfaceTint: "#89dceb" },
|
||||
light: { primary: "#04a5e5", secondary: "#209fb5", primaryText: "#ffffff", primaryContainer: "#dbf1fb", surfaceTint: "#04a5e5" }
|
||||
},
|
||||
"cat-sapphire": {
|
||||
name: "Sapphire",
|
||||
dark: { primary: "#74c7ec", secondary: "#89b4fa", primaryText: "#1e1e2e", primaryContainer: "#1f4d6f", surfaceTint: "#74c7ec" },
|
||||
light: { primary: "#209fb5", secondary: "#1e66f5", primaryText: "#ffffff", primaryContainer: "#def3f8", surfaceTint: "#209fb5" }
|
||||
},
|
||||
"cat-blue": {
|
||||
name: "Blue",
|
||||
dark: { primary: "#89b4fa", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#243f75", surfaceTint: "#89b4fa" },
|
||||
light: { primary: "#1e66f5", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#e0e9ff", surfaceTint: "#1e66f5" }
|
||||
},
|
||||
"cat-lavender": {
|
||||
name: "Lavender",
|
||||
dark: { primary: "#b4befe", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#3f4481", surfaceTint: "#b4befe" },
|
||||
light: { primary: "#7287fd", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#e5e8ff", surfaceTint: "#7287fd" }
|
||||
}
|
||||
}
|
||||
|
||||
function getCatppuccinTheme(variant, isLight = false) {
|
||||
const variantData = CatppuccinVariants[variant]
|
||||
if (!variantData) return null
|
||||
|
||||
const baseColors = isLight ? CatppuccinLatte : CatppuccinMocha
|
||||
const accentColors = isLight ? variantData.light : variantData.dark
|
||||
|
||||
return Object.assign({
|
||||
name: `${variantData.name}${isLight ? ' Light' : ''}`
|
||||
}, baseColors, accentColors)
|
||||
}
|
||||
|
||||
const StockThemes = {
|
||||
DARK: {
|
||||
blue: {
|
||||
name: "Blue",
|
||||
primary: "#42a5f5",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#0d47a1",
|
||||
secondary: "#8ab4f8",
|
||||
surface: "#101418",
|
||||
surfaceText: "#e0e2e8",
|
||||
surfaceVariant: "#42474e",
|
||||
surfaceVariantText: "#c2c7cf",
|
||||
surfaceTint: "#8ab4f8",
|
||||
background: "#101418",
|
||||
backgroundText: "#e0e2e8",
|
||||
outline: "#8c9199",
|
||||
surfaceContainer: "#1d2024",
|
||||
surfaceContainerHigh: "#272a2f",
|
||||
surfaceContainerHighest: "#32353a"
|
||||
},
|
||||
purple: {
|
||||
name: "Purple",
|
||||
primary: "#D0BCFF",
|
||||
primaryText: "#381E72",
|
||||
primaryContainer: "#4F378B",
|
||||
secondary: "#CCC2DC",
|
||||
surface: "#141218",
|
||||
surfaceText: "#e6e0e9",
|
||||
surfaceVariant: "#49454e",
|
||||
surfaceVariantText: "#cac4cf",
|
||||
surfaceTint: "#D0BCFF",
|
||||
background: "#141218",
|
||||
backgroundText: "#e6e0e9",
|
||||
outline: "#948f99",
|
||||
surfaceContainer: "#211f24",
|
||||
surfaceContainerHigh: "#2b292f",
|
||||
surfaceContainerHighest: "#36343a"
|
||||
},
|
||||
green: {
|
||||
name: "Green",
|
||||
primary: "#4caf50",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#1b5e20",
|
||||
secondary: "#81c995",
|
||||
surface: "#10140f",
|
||||
surfaceText: "#e0e4db",
|
||||
surfaceVariant: "#424940",
|
||||
surfaceVariantText: "#c2c9bd",
|
||||
surfaceTint: "#81c995",
|
||||
background: "#10140f",
|
||||
backgroundText: "#e0e4db",
|
||||
outline: "#8c9388",
|
||||
surfaceContainer: "#1d211b",
|
||||
surfaceContainerHigh: "#272b25",
|
||||
surfaceContainerHighest: "#323630"
|
||||
},
|
||||
orange: {
|
||||
name: "Orange",
|
||||
primary: "#ff6d00",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#3e2723",
|
||||
secondary: "#ffb74d",
|
||||
surface: "#1a120e",
|
||||
surfaceText: "#f0dfd8",
|
||||
surfaceVariant: "#52443d",
|
||||
surfaceVariantText: "#d7c2b9",
|
||||
surfaceTint: "#ffb74d",
|
||||
background: "#1a120e",
|
||||
backgroundText: "#f0dfd8",
|
||||
outline: "#a08d85",
|
||||
surfaceContainer: "#271e1a",
|
||||
surfaceContainerHigh: "#322824",
|
||||
surfaceContainerHighest: "#3d332e"
|
||||
},
|
||||
red: {
|
||||
name: "Red",
|
||||
primary: "#f44336",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#4a0e0e",
|
||||
secondary: "#f28b82",
|
||||
surface: "#1a1110",
|
||||
surfaceText: "#f1dedc",
|
||||
surfaceVariant: "#534341",
|
||||
surfaceVariantText: "#d8c2be",
|
||||
surfaceTint: "#f28b82",
|
||||
background: "#1a1110",
|
||||
backgroundText: "#f1dedc",
|
||||
outline: "#a08c89",
|
||||
surfaceContainer: "#271d1c",
|
||||
surfaceContainerHigh: "#322826",
|
||||
surfaceContainerHighest: "#3d3231"
|
||||
},
|
||||
cyan: {
|
||||
name: "Cyan",
|
||||
primary: "#00bcd4",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#004d5c",
|
||||
secondary: "#4dd0e1",
|
||||
surface: "#0e1416",
|
||||
surfaceText: "#dee3e5",
|
||||
surfaceVariant: "#3f484a",
|
||||
surfaceVariantText: "#bfc8ca",
|
||||
surfaceTint: "#4dd0e1",
|
||||
background: "#0e1416",
|
||||
backgroundText: "#dee3e5",
|
||||
outline: "#899295",
|
||||
surfaceContainer: "#1b2122",
|
||||
surfaceContainerHigh: "#252b2c",
|
||||
surfaceContainerHighest: "#303637"
|
||||
},
|
||||
pink: {
|
||||
name: "Pink",
|
||||
primary: "#e91e63",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#4a0e2f",
|
||||
secondary: "#f8bbd9",
|
||||
surface: "#191112",
|
||||
surfaceText: "#f0dee0",
|
||||
surfaceVariant: "#524345",
|
||||
surfaceVariantText: "#d6c2c3",
|
||||
surfaceTint: "#f8bbd9",
|
||||
background: "#191112",
|
||||
backgroundText: "#f0dee0",
|
||||
outline: "#9f8c8e",
|
||||
surfaceContainer: "#261d1e",
|
||||
surfaceContainerHigh: "#312829",
|
||||
surfaceContainerHighest: "#3c3233"
|
||||
},
|
||||
amber: {
|
||||
name: "Amber",
|
||||
primary: "#ffc107",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#4a3c00",
|
||||
secondary: "#ffd54f",
|
||||
surface: "#17130b",
|
||||
surfaceText: "#ebe1d4",
|
||||
surfaceVariant: "#4d4639",
|
||||
surfaceVariantText: "#d0c5b4",
|
||||
surfaceTint: "#ffd54f",
|
||||
background: "#17130b",
|
||||
backgroundText: "#ebe1d4",
|
||||
outline: "#998f80",
|
||||
surfaceContainer: "#231f17",
|
||||
surfaceContainerHigh: "#2e2921",
|
||||
surfaceContainerHighest: "#39342b"
|
||||
},
|
||||
coral: {
|
||||
name: "Coral",
|
||||
primary: "#ffb4ab",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#8c1d18",
|
||||
secondary: "#f9dedc",
|
||||
surface: "#1a1110",
|
||||
surfaceText: "#f1dedc",
|
||||
surfaceVariant: "#534341",
|
||||
surfaceVariantText: "#d8c2bf",
|
||||
surfaceTint: "#ffb4ab",
|
||||
background: "#1a1110",
|
||||
backgroundText: "#f1dedc",
|
||||
outline: "#a08c8a",
|
||||
surfaceContainer: "#271d1c",
|
||||
surfaceContainerHigh: "#322826",
|
||||
surfaceContainerHighest: "#3d3231"
|
||||
},
|
||||
monochrome: {
|
||||
name: "Monochrome",
|
||||
primary: "#ffffff",
|
||||
primaryText: "#2b303c",
|
||||
primaryContainer: "#424753",
|
||||
secondary: "#c4c6d0",
|
||||
surface: "#2a2a2a",
|
||||
surfaceText: "#e4e2e3",
|
||||
surfaceVariant: "#474648",
|
||||
surfaceVariantText: "#c8c6c7",
|
||||
surfaceTint: "#c2c6d6",
|
||||
background: "#131315",
|
||||
backgroundText: "#e4e2e3",
|
||||
outline: "#929092",
|
||||
surfaceContainer: "#353535",
|
||||
surfaceContainerHigh: "#424242",
|
||||
surfaceContainerHighest: "#505050",
|
||||
error: "#ffb4ab",
|
||||
warning: "#3f4759",
|
||||
info: "#595e6c",
|
||||
matugen_type: "scheme-monochrome"
|
||||
}
|
||||
},
|
||||
LIGHT: {
|
||||
blue: {
|
||||
name: "Blue Light",
|
||||
primary: "#1976d2",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#e3f2fd",
|
||||
secondary: "#42a5f5",
|
||||
surface: "#f7f9ff",
|
||||
surfaceText: "#181c20",
|
||||
surfaceVariant: "#dee3eb",
|
||||
surfaceVariantText: "#42474e",
|
||||
surfaceTint: "#1976d2",
|
||||
background: "#f7f9ff",
|
||||
backgroundText: "#181c20",
|
||||
outline: "#72777f",
|
||||
surfaceContainer: "#eceef4",
|
||||
surfaceContainerHigh: "#e6e8ee",
|
||||
surfaceContainerHighest: "#e0e2e8"
|
||||
},
|
||||
purple: {
|
||||
name: "Purple Light",
|
||||
primary: "#6750A4",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#EADDFF",
|
||||
secondary: "#625B71",
|
||||
surface: "#fef7ff",
|
||||
surfaceText: "#1d1b20",
|
||||
surfaceVariant: "#e7e0eb",
|
||||
surfaceVariantText: "#49454e",
|
||||
surfaceTint: "#6750A4",
|
||||
background: "#fef7ff",
|
||||
backgroundText: "#1d1b20",
|
||||
outline: "#7a757f",
|
||||
surfaceContainer: "#f2ecf4",
|
||||
surfaceContainerHigh: "#ece6ee",
|
||||
surfaceContainerHighest: "#e6e0e9"
|
||||
},
|
||||
green: {
|
||||
name: "Green Light",
|
||||
primary: "#2e7d32",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#e8f5e8",
|
||||
secondary: "#4caf50",
|
||||
surface: "#f7fbf1",
|
||||
surfaceText: "#191d17",
|
||||
surfaceVariant: "#dee5d8",
|
||||
surfaceVariantText: "#424940",
|
||||
surfaceTint: "#2e7d32",
|
||||
background: "#f7fbf1",
|
||||
backgroundText: "#191d17",
|
||||
outline: "#72796f",
|
||||
surfaceContainer: "#ecefe6",
|
||||
surfaceContainerHigh: "#e6e9e0",
|
||||
surfaceContainerHighest: "#e0e4db"
|
||||
},
|
||||
orange: {
|
||||
name: "Orange Light",
|
||||
primary: "#e65100",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#ffecb3",
|
||||
secondary: "#ff9800",
|
||||
surface: "#fff8f6",
|
||||
surfaceText: "#221a16",
|
||||
surfaceVariant: "#f4ded5",
|
||||
surfaceVariantText: "#52443d",
|
||||
surfaceTint: "#e65100",
|
||||
background: "#fff8f6",
|
||||
backgroundText: "#221a16",
|
||||
outline: "#85736c",
|
||||
surfaceContainer: "#fceae3",
|
||||
surfaceContainerHigh: "#f6e5de",
|
||||
surfaceContainerHighest: "#f0dfd8"
|
||||
},
|
||||
red: {
|
||||
name: "Red Light",
|
||||
primary: "#d32f2f",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#ffebee",
|
||||
secondary: "#f44336",
|
||||
surface: "#fff8f7",
|
||||
surfaceText: "#231918",
|
||||
surfaceVariant: "#f5ddda",
|
||||
surfaceVariantText: "#534341",
|
||||
surfaceTint: "#d32f2f",
|
||||
background: "#fff8f7",
|
||||
backgroundText: "#231918",
|
||||
outline: "#857370",
|
||||
surfaceContainer: "#fceae7",
|
||||
surfaceContainerHigh: "#f7e4e1",
|
||||
surfaceContainerHighest: "#f1dedc"
|
||||
},
|
||||
cyan: {
|
||||
name: "Cyan Light",
|
||||
primary: "#0097a7",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#e0f2f1",
|
||||
secondary: "#00bcd4",
|
||||
surface: "#f5fafc",
|
||||
surfaceText: "#171d1e",
|
||||
surfaceVariant: "#dbe4e6",
|
||||
surfaceVariantText: "#3f484a",
|
||||
surfaceTint: "#0097a7",
|
||||
background: "#f5fafc",
|
||||
backgroundText: "#171d1e",
|
||||
outline: "#6f797b",
|
||||
surfaceContainer: "#e9eff0",
|
||||
surfaceContainerHigh: "#e3e9eb",
|
||||
surfaceContainerHighest: "#dee3e5"
|
||||
},
|
||||
pink: {
|
||||
name: "Pink Light",
|
||||
primary: "#c2185b",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#fce4ec",
|
||||
secondary: "#e91e63",
|
||||
surface: "#fff8f7",
|
||||
surfaceText: "#22191a",
|
||||
surfaceVariant: "#f3dddf",
|
||||
surfaceVariantText: "#524345",
|
||||
surfaceTint: "#c2185b",
|
||||
background: "#fff8f7",
|
||||
backgroundText: "#22191a",
|
||||
outline: "#847375",
|
||||
surfaceContainer: "#fbeaeb",
|
||||
surfaceContainerHigh: "#f5e4e5",
|
||||
surfaceContainerHighest: "#f0dee0"
|
||||
},
|
||||
amber: {
|
||||
name: "Amber Light",
|
||||
primary: "#ff8f00",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#fff8e1",
|
||||
secondary: "#ffc107",
|
||||
surface: "#fff8f2",
|
||||
surfaceText: "#1f1b13",
|
||||
surfaceVariant: "#ede1cf",
|
||||
surfaceVariantText: "#4d4639",
|
||||
surfaceTint: "#ff8f00",
|
||||
background: "#fff8f2",
|
||||
backgroundText: "#1f1b13",
|
||||
outline: "#7f7667",
|
||||
surfaceContainer: "#f6ecdf",
|
||||
surfaceContainerHigh: "#f1e7d9",
|
||||
surfaceContainerHighest: "#ebe1d4"
|
||||
},
|
||||
coral: {
|
||||
name: "Coral Light",
|
||||
primary: "#8c1d18",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#ffdad6",
|
||||
secondary: "#ff5449",
|
||||
surface: "#fff8f7",
|
||||
surfaceText: "#231918",
|
||||
surfaceVariant: "#f5ddda",
|
||||
surfaceVariantText: "#534341",
|
||||
surfaceTint: "#8c1d18",
|
||||
background: "#fff8f7",
|
||||
backgroundText: "#231918",
|
||||
outline: "#857371",
|
||||
surfaceContainer: "#fceae7",
|
||||
surfaceContainerHigh: "#f6e4e2",
|
||||
surfaceContainerHighest: "#f1dedc"
|
||||
},
|
||||
monochrome: {
|
||||
name: "Monochrome Light",
|
||||
primary: "#2b303c",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#d6d7dc",
|
||||
secondary: "#4a4d56",
|
||||
surface: "#f5f5f6",
|
||||
surfaceText: "#2a2a2a",
|
||||
surfaceVariant: "#e0e0e2",
|
||||
surfaceVariantText: "#424242",
|
||||
surfaceTint: "#5a5f6e",
|
||||
background: "#ffffff",
|
||||
backgroundText: "#1a1a1a",
|
||||
outline: "#757577",
|
||||
surfaceContainer: "#e8e8ea",
|
||||
surfaceContainerHigh: "#dcdcde",
|
||||
surfaceContainerHighest: "#d0d0d2",
|
||||
error: "#ba1a1a",
|
||||
warning: "#f9e79f",
|
||||
info: "#5d6475",
|
||||
matugen_type: "scheme-monochrome"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ThemeCategories = {
|
||||
GENERIC: {
|
||||
name: "Generic",
|
||||
variants: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"]
|
||||
},
|
||||
CATPPUCCIN: {
|
||||
name: "Catppuccin",
|
||||
variants: Object.keys(CatppuccinVariants)
|
||||
}
|
||||
}
|
||||
|
||||
const ThemeNames = {
|
||||
BLUE: "blue",
|
||||
PURPLE: "purple",
|
||||
GREEN: "green",
|
||||
ORANGE: "orange",
|
||||
RED: "red",
|
||||
CYAN: "cyan",
|
||||
PINK: "pink",
|
||||
AMBER: "amber",
|
||||
CORAL: "coral",
|
||||
MONOCHROME: "monochrome",
|
||||
DYNAMIC: "dynamic"
|
||||
}
|
||||
|
||||
function isStockTheme(themeName) {
|
||||
return Object.keys(StockThemes.DARK).includes(themeName)
|
||||
}
|
||||
|
||||
function isCatppuccinVariant(themeName) {
|
||||
return Object.keys(CatppuccinVariants).includes(themeName)
|
||||
}
|
||||
|
||||
function getAvailableThemes(isLight = false) {
|
||||
return isLight ? StockThemes.LIGHT : StockThemes.DARK
|
||||
}
|
||||
|
||||
function getThemeByName(themeName, isLight = false) {
|
||||
if (isCatppuccinVariant(themeName)) {
|
||||
return getCatppuccinTheme(themeName, isLight)
|
||||
}
|
||||
const themes = getAvailableThemes(isLight)
|
||||
return themes[themeName] || themes.blue
|
||||
}
|
||||
|
||||
function getAllThemeNames() {
|
||||
return Object.keys(StockThemes.DARK)
|
||||
}
|
||||
|
||||
function getCatppuccinVariantNames() {
|
||||
return Object.keys(CatppuccinVariants)
|
||||
}
|
||||
|
||||
function getThemeCategories() {
|
||||
return ThemeCategories
|
||||
}
|
||||
1130
Common/Theme.qml
1130
Common/Theme.qml
File diff suppressed because it is too large
Load Diff
@@ -1,56 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function init(leftModel, centerModel, rightModel, left, center, right) {
|
||||
const dummy = {
|
||||
widgetId: "dummy",
|
||||
enabled: true,
|
||||
size: 20,
|
||||
selectedGpuIndex: 0,
|
||||
pciId: "",
|
||||
mountPath: "/",
|
||||
minimumWidth: true,
|
||||
showSwap: false
|
||||
}
|
||||
leftModel.append(dummy)
|
||||
centerModel.append(dummy)
|
||||
rightModel.append(dummy)
|
||||
|
||||
update(leftModel, left)
|
||||
update(centerModel, center)
|
||||
update(rightModel, right)
|
||||
}
|
||||
|
||||
function update(model, order) {
|
||||
model.clear()
|
||||
for (var i = 0; i < order.length; i++) {
|
||||
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id
|
||||
var enabled = typeof order[i] === "string" ? true : order[i].enabled
|
||||
var size = typeof order[i] === "string" ? undefined : order[i].size
|
||||
var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex
|
||||
var pciId = typeof order[i] === "string" ? undefined : order[i].pciId
|
||||
var mountPath = typeof order[i] === "string" ? undefined : order[i].mountPath
|
||||
var minimumWidth = typeof order[i] === "string" ? undefined : order[i].minimumWidth
|
||||
var showSwap = typeof order[i] === "string" ? undefined : order[i].showSwap
|
||||
var item = {
|
||||
widgetId: widgetId,
|
||||
enabled: enabled
|
||||
}
|
||||
if (size !== undefined) item.size = size
|
||||
if (selectedGpuIndex !== undefined) item.selectedGpuIndex = selectedGpuIndex
|
||||
if (pciId !== undefined) item.pciId = pciId
|
||||
if (mountPath !== undefined) item.mountPath = mountPath
|
||||
if (minimumWidth !== undefined) item.minimumWidth = minimumWidth
|
||||
if (showSwap !== undefined) item.showSwap = showSwap
|
||||
|
||||
model.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
.import "./SettingsSpec.js" as SpecModule
|
||||
|
||||
function parse(root, jsonObj) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
for (var k in SPEC) {
|
||||
var spec = SPEC[k];
|
||||
root[k] = spec.def;
|
||||
}
|
||||
|
||||
if (!jsonObj) return;
|
||||
|
||||
for (var k in jsonObj) {
|
||||
if (!SPEC[k]) continue;
|
||||
var raw = jsonObj[k];
|
||||
var spec = SPEC[k];
|
||||
var coerce = spec.coerce;
|
||||
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
|
||||
}
|
||||
}
|
||||
|
||||
function toJson(root) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
var out = {};
|
||||
for (var k in SPEC) {
|
||||
if (SPEC[k].persist === false) continue;
|
||||
out[k] = root[k];
|
||||
}
|
||||
out.configVersion = root.settingsConfigVersion;
|
||||
return out;
|
||||
}
|
||||
|
||||
function migrate(root, jsonObj) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
if (!jsonObj) return;
|
||||
|
||||
if (jsonObj.themeIndex !== undefined || jsonObj.themeIsDynamic !== undefined) {
|
||||
var themeNames = ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"];
|
||||
if (jsonObj.themeIsDynamic) {
|
||||
root.currentThemeName = "dynamic";
|
||||
} else if (jsonObj.themeIndex >= 0 && jsonObj.themeIndex < themeNames.length) {
|
||||
root.currentThemeName = themeNames[jsonObj.themeIndex];
|
||||
}
|
||||
console.info("Auto-migrated theme from index", jsonObj.themeIndex, "to", root.currentThemeName);
|
||||
}
|
||||
|
||||
if ((jsonObj.dankBarWidgetOrder && jsonObj.dankBarWidgetOrder.length > 0) ||
|
||||
(jsonObj.topBarWidgetOrder && jsonObj.topBarWidgetOrder.length > 0)) {
|
||||
if (jsonObj.dankBarLeftWidgets === undefined && jsonObj.dankBarCenterWidgets === undefined && jsonObj.dankBarRightWidgets === undefined) {
|
||||
var widgetOrder = jsonObj.dankBarWidgetOrder || jsonObj.topBarWidgetOrder;
|
||||
root.dankBarLeftWidgets = widgetOrder.filter(function(w) { return ["launcherButton", "workspaceSwitcher", "focusedWindow"].indexOf(w) >= 0; });
|
||||
root.dankBarCenterWidgets = widgetOrder.filter(function(w) { return ["clock", "music", "weather"].indexOf(w) >= 0; });
|
||||
root.dankBarRightWidgets = widgetOrder.filter(function(w) { return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].indexOf(w) >= 0; });
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObj.useOSLogo !== undefined) {
|
||||
root.launcherLogoMode = jsonObj.useOSLogo ? "os" : "apps";
|
||||
root.launcherLogoColorOverride = jsonObj.osLogoColorOverride !== undefined ? jsonObj.osLogoColorOverride : "";
|
||||
root.launcherLogoBrightness = jsonObj.osLogoBrightness !== undefined ? jsonObj.osLogoBrightness : 0.5;
|
||||
root.launcherLogoContrast = jsonObj.osLogoContrast !== undefined ? jsonObj.osLogoContrast : 1;
|
||||
}
|
||||
|
||||
if (jsonObj.mediaCompactMode !== undefined && jsonObj.mediaSize === undefined) {
|
||||
root.mediaSize = jsonObj.mediaCompactMode ? 0 : 1;
|
||||
}
|
||||
|
||||
for (var k in SPEC) {
|
||||
var spec = SPEC[k];
|
||||
if (!spec.migrate) continue;
|
||||
for (var i = 0; i < spec.migrate.length; i++) {
|
||||
var oldKey = spec.migrate[i];
|
||||
if (jsonObj[oldKey] !== undefined && jsonObj[k] === undefined) {
|
||||
var raw = jsonObj[oldKey];
|
||||
var coerce = spec.coerce;
|
||||
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObj.dankBarAtBottom !== undefined || jsonObj.topBarAtBottom !== undefined) {
|
||||
var atBottom = jsonObj.dankBarAtBottom !== undefined ? jsonObj.dankBarAtBottom : jsonObj.topBarAtBottom;
|
||||
root.dankBarPosition = atBottom ? 1 : 0;
|
||||
}
|
||||
|
||||
if (jsonObj.pluginSettings !== undefined) {
|
||||
root.pluginSettings = jsonObj.pluginSettings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function cleanup(fileText) {
|
||||
var getValidKeys = SpecModule.getValidKeys;
|
||||
if (!fileText || !fileText.trim()) return;
|
||||
|
||||
try {
|
||||
var settings = JSON.parse(fileText);
|
||||
var validKeys = getValidKeys();
|
||||
var needsSave = false;
|
||||
|
||||
for (var key in settings) {
|
||||
if (validKeys.indexOf(key) < 0) {
|
||||
console.log("SettingsData: Removing unused key:", key);
|
||||
delete settings[key];
|
||||
needsSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
return needsSave ? JSON.stringify(settings, null, 2) : null;
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to cleanup unused keys:", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
588
DMSShell.qml
588
DMSShell.qml
@@ -1,588 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.Settings
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Modules.DankDash
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Modules.Dock
|
||||
import qs.Modules.Lock
|
||||
import qs.Modules.Notepad
|
||||
import qs.Modules.Notifications.Center
|
||||
import qs.Widgets
|
||||
import qs.Modules.Notifications.Popup
|
||||
import qs.Modules.OSD
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankBar.Popouts
|
||||
import qs.Modules.HyprWorkspaces
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Instantiator {
|
||||
id: daemonPluginInstantiator
|
||||
asynchronous: true
|
||||
model: Object.keys(PluginService.pluginDaemonComponents)
|
||||
|
||||
delegate: Loader {
|
||||
id: daemonLoader
|
||||
property string pluginId: modelData
|
||||
sourceComponent: PluginService.pluginDaemonComponents[pluginId]
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
item.pluginService = PluginService
|
||||
if (item.popoutService !== undefined) {
|
||||
item.popoutService = PopoutService
|
||||
}
|
||||
item.pluginId = pluginId
|
||||
console.info("Daemon plugin loaded:", pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: blurredWallpaperBackgroundLoader
|
||||
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
|
||||
asynchronous: false
|
||||
|
||||
sourceComponent: BlurredWallpaperBackground {}
|
||||
}
|
||||
|
||||
WallpaperBackground {}
|
||||
|
||||
Lock {
|
||||
id: lock
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dankBarLoader
|
||||
asynchronous: false
|
||||
|
||||
property var currentPosition: SettingsData.dankBarPosition
|
||||
property bool initialized: false
|
||||
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
|
||||
|
||||
sourceComponent: DankBar {
|
||||
hyprlandOverviewLoader: dankBarLoader.hyprlandOverviewLoaderRef
|
||||
|
||||
onColorPickerRequested: {
|
||||
if (colorPickerModal.shouldBeVisible) {
|
||||
colorPickerModal.close()
|
||||
} else {
|
||||
colorPickerModal.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
initialized = true
|
||||
}
|
||||
|
||||
onCurrentPositionChanged: {
|
||||
if (!initialized)
|
||||
return
|
||||
|
||||
const component = sourceComponent
|
||||
sourceComponent = null
|
||||
sourceComponent = component
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dockLoader
|
||||
active: true
|
||||
asynchronous: false
|
||||
|
||||
property var currentPosition: SettingsData.dockPosition
|
||||
property bool initialized: false
|
||||
|
||||
sourceComponent: Dock {
|
||||
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
dockContextMenuLoader.active = true
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
initialized = true
|
||||
}
|
||||
|
||||
onCurrentPositionChanged: {
|
||||
if (!initialized)
|
||||
return
|
||||
|
||||
console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock")
|
||||
const comp = sourceComponent
|
||||
sourceComponent = null
|
||||
sourceComponent = comp
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dankDashPopoutLoader
|
||||
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Component {
|
||||
DankDashPopout {
|
||||
id: dankDashPopout
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.dankDashPopout = dankDashPopout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: dockContextMenuLoader
|
||||
|
||||
active: false
|
||||
|
||||
DockContextMenu {
|
||||
id: dockContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: notificationCenterLoader
|
||||
|
||||
active: false
|
||||
|
||||
NotificationCenterPopout {
|
||||
id: notificationCenter
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.notificationCenterPopout = notificationCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("notifications")
|
||||
|
||||
delegate: NotificationPopupManager {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: controlCenterLoader
|
||||
|
||||
active: false
|
||||
|
||||
property var modalRef: colorPickerModal
|
||||
property LazyLoader powerModalLoaderRef: powerMenuModalLoader
|
||||
|
||||
ControlCenterPopout {
|
||||
id: controlCenterPopout
|
||||
colorPickerModal: controlCenterLoader.modalRef
|
||||
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
|
||||
|
||||
onLockRequested: {
|
||||
lock.activate()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.controlCenterPopout = controlCenterPopout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WifiPasswordModal {
|
||||
id: wifiPasswordModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.wifiPasswordModal = wifiPasswordModal
|
||||
}
|
||||
}
|
||||
|
||||
PolkitAuthModal {
|
||||
id: polkitAuthModal
|
||||
}
|
||||
|
||||
property string lastCredentialsToken: ""
|
||||
property var lastCredentialsTime: 0
|
||||
|
||||
Connections {
|
||||
target: NetworkService
|
||||
|
||||
function onCredentialsNeeded(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
|
||||
const now = Date.now()
|
||||
const timeSinceLastPrompt = now - lastCredentialsTime
|
||||
|
||||
if (wifiPasswordModal.shouldBeVisible && timeSinceLastPrompt < 1000) {
|
||||
NetworkService.cancelCredentials(lastCredentialsToken)
|
||||
lastCredentialsToken = token
|
||||
lastCredentialsTime = now
|
||||
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService)
|
||||
return
|
||||
}
|
||||
|
||||
lastCredentialsToken = token
|
||||
lastCredentialsTime = now
|
||||
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService)
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: networkInfoModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
NetworkInfoModal {
|
||||
id: networkInfoModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.networkInfoModal = networkInfoModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: batteryPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
BatteryPopout {
|
||||
id: batteryPopout
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.batteryPopout = batteryPopout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: vpnPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
VpnPopout {
|
||||
id: vpnPopout
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.vpnPopout = vpnPopout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerMenuLoader
|
||||
|
||||
active: false
|
||||
|
||||
PowerMenu {
|
||||
id: powerMenu
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
if (SettingsData.powerActionConfirm) {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
|
||||
}
|
||||
} else {
|
||||
actionApply(action)
|
||||
}
|
||||
}
|
||||
|
||||
function actionApply(action) {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerConfirmModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
ConfirmModal {
|
||||
id: powerConfirmModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: processListPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
ProcessListPopout {
|
||||
id: processListPopout
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.processListPopout = processListPopout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsModal {
|
||||
id: settingsModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.settingsModal = settingsModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: appDrawerLoader
|
||||
|
||||
active: false
|
||||
|
||||
AppDrawerPopout {
|
||||
id: appDrawerPopout
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.appDrawerPopout = appDrawerPopout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightModal {
|
||||
id: spotlightModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.spotlightModal = spotlightModal
|
||||
}
|
||||
}
|
||||
|
||||
ClipboardHistoryModal {
|
||||
id: clipboardHistoryModalPopup
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.clipboardHistoryModal = clipboardHistoryModalPopup
|
||||
}
|
||||
}
|
||||
|
||||
NotificationModal {
|
||||
id: notificationModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.notificationModal = notificationModal
|
||||
}
|
||||
}
|
||||
|
||||
DankColorPickerModal {
|
||||
id: colorPickerModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.colorPickerModal = colorPickerModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: processListModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
ProcessListModal {
|
||||
id: processListModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.processListModal = processListModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: systemUpdateLoader
|
||||
|
||||
active: false
|
||||
|
||||
SystemUpdatePopout {
|
||||
id: systemUpdatePopout
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.systemUpdatePopout = systemUpdatePopout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: notepadSlideoutVariants
|
||||
model: SettingsData.getFilteredScreens("notepad")
|
||||
|
||||
delegate: DankSlideout {
|
||||
id: notepadSlideout
|
||||
modelData: item
|
||||
title: I18n.tr("Notepad")
|
||||
slideoutWidth: 480
|
||||
expandable: true
|
||||
expandedWidthValue: 960
|
||||
customTransparency: SettingsData.notepadTransparencyOverride
|
||||
|
||||
content: Component {
|
||||
Notepad {
|
||||
onHideRequested: {
|
||||
notepadSlideout.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isVisible) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerMenuModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
PowerMenuModal {
|
||||
id: powerMenuModal
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
if (SettingsData.powerActionConfirm) {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
|
||||
}
|
||||
} else {
|
||||
actionApply(action)
|
||||
}
|
||||
}
|
||||
|
||||
onLockRequested: {
|
||||
lock.activate()
|
||||
}
|
||||
|
||||
function actionApply(action) {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.powerMenuModal = powerMenuModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: hyprKeybindsModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
KeybindsModal {
|
||||
id: keybindsModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.hyprKeybindsModal = keybindsModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DMSShellIPC {
|
||||
powerMenuModalLoader: powerMenuModalLoader
|
||||
processListModalLoader: processListModalLoader
|
||||
controlCenterLoader: controlCenterLoader
|
||||
dankDashPopoutLoader: dankDashPopoutLoader
|
||||
notepadSlideoutVariants: notepadSlideoutVariants
|
||||
hyprKeybindsModalLoader: hyprKeybindsModalLoader
|
||||
dankBarLoader: dankBarLoader
|
||||
hyprlandOverviewLoader: hyprlandOverviewLoader
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("toast")
|
||||
|
||||
delegate: Toast {
|
||||
modelData: item
|
||||
visible: ToastService.toastVisible
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: VolumeOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: MicMuteOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: BrightnessOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: IdleInhibitorOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: hyprlandOverviewLoader
|
||||
active: CompositorService.isHyprland
|
||||
component: HyprlandOverview {
|
||||
id: hyprlandOverview
|
||||
}
|
||||
}
|
||||
}
|
||||
434
DMSShellIPC.qml
434
DMSShellIPC.qml
@@ -1,434 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var powerMenuModalLoader
|
||||
required property var processListModalLoader
|
||||
required property var controlCenterLoader
|
||||
required property var dankDashPopoutLoader
|
||||
required property var notepadSlideoutVariants
|
||||
required property var hyprKeybindsModalLoader
|
||||
required property var dankBarLoader
|
||||
required property var hyprlandOverviewLoader
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
root.powerMenuModalLoader.active = true
|
||||
if (root.powerMenuModalLoader.item)
|
||||
root.powerMenuModalLoader.item.openCentered()
|
||||
|
||||
return "POWERMENU_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (root.powerMenuModalLoader.item)
|
||||
root.powerMenuModalLoader.item.close()
|
||||
|
||||
return "POWERMENU_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
root.powerMenuModalLoader.active = true
|
||||
if (root.powerMenuModalLoader.item) {
|
||||
if (root.powerMenuModalLoader.item.shouldBeVisible) {
|
||||
root.powerMenuModalLoader.item.close()
|
||||
} else {
|
||||
root.powerMenuModalLoader.item.openCentered()
|
||||
}
|
||||
}
|
||||
|
||||
return "POWERMENU_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "powermenu"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
root.processListModalLoader.active = true
|
||||
if (root.processListModalLoader.item)
|
||||
root.processListModalLoader.item.show()
|
||||
|
||||
return "PROCESSLIST_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.processListModalLoader.item)
|
||||
root.processListModalLoader.item.hide()
|
||||
|
||||
return "PROCESSLIST_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
root.processListModalLoader.active = true
|
||||
if (root.processListModalLoader.item)
|
||||
root.processListModalLoader.item.toggle()
|
||||
|
||||
return "PROCESSLIST_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "processlist"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
if (root.dankBarLoader.item) {
|
||||
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
|
||||
return "CONTROL_CENTER_OPEN_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.controlCenterLoader.item) {
|
||||
root.controlCenterLoader.item.close()
|
||||
return "CONTROL_CENTER_CLOSE_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
if (root.dankBarLoader.item) {
|
||||
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
|
||||
return "CONTROL_CENTER_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "control-center"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(tab: string): string {
|
||||
root.dankDashPopoutLoader.active = true
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 1
|
||||
break
|
||||
case "weather":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
||||
break
|
||||
default:
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 0
|
||||
break
|
||||
}
|
||||
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
||||
root.dankDashPopoutLoader.item.dashVisible = true
|
||||
return "DASH_OPEN_SUCCESS"
|
||||
}
|
||||
return "DASH_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
root.dankDashPopoutLoader.item.dashVisible = false
|
||||
return "DASH_CLOSE_SUCCESS"
|
||||
}
|
||||
return "DASH_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(tab: string): string {
|
||||
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 1
|
||||
break
|
||||
case "wallpaper":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 2
|
||||
break
|
||||
case "weather":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0
|
||||
break
|
||||
default:
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
return "DASH_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "DASH_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "dash"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function getFocusedScreenName() {
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
||||
return Hyprland.focusedWorkspace.monitor.name
|
||||
}
|
||||
if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||
return NiriService.currentOutput
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function getActiveNotepadInstance() {
|
||||
if (root.notepadSlideoutVariants.instances.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (root.notepadSlideoutVariants.instances.length === 1) {
|
||||
return root.notepadSlideoutVariants.instances[0]
|
||||
}
|
||||
|
||||
var focusedScreen = getFocusedScreenName()
|
||||
if (focusedScreen && root.notepadSlideoutVariants.instances.length > 0) {
|
||||
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
|
||||
var slideout = root.notepadSlideoutVariants.instances[i]
|
||||
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
|
||||
return slideout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
|
||||
var slideout = root.notepadSlideoutVariants.instances[i]
|
||||
if (slideout.isVisible) {
|
||||
return slideout
|
||||
}
|
||||
}
|
||||
|
||||
return root.notepadSlideoutVariants.instances[0]
|
||||
}
|
||||
|
||||
function open(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.show()
|
||||
return "NOTEPAD_OPEN_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.hide()
|
||||
return "NOTEPAD_CLOSE_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.toggle()
|
||||
return "NOTEPAD_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "notepad"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle(): string {
|
||||
SessionService.toggleIdleInhibit()
|
||||
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
|
||||
}
|
||||
|
||||
function enable(): string {
|
||||
SessionService.enableIdleInhibit()
|
||||
return "Idle inhibit enabled"
|
||||
}
|
||||
|
||||
function disable(): string {
|
||||
SessionService.disableIdleInhibit()
|
||||
return "Idle inhibit disabled"
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
return SessionService.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
|
||||
}
|
||||
|
||||
function reason(newReason: string): string {
|
||||
if (!newReason) {
|
||||
return `Current reason: ${SessionService.inhibitReason}`
|
||||
}
|
||||
|
||||
SessionService.setInhibitReason(newReason)
|
||||
return `Inhibit reason set to: ${newReason}`
|
||||
}
|
||||
|
||||
target: "inhibit"
|
||||
}
|
||||
|
||||
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 toggle(provider: string): string {
|
||||
if (!provider) {
|
||||
return "ERROR: No provider specified"
|
||||
}
|
||||
|
||||
KeybindsService.loadProvider(provider)
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
} else {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
}
|
||||
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`
|
||||
}
|
||||
return `KEYBINDS_TOGGLE_FAILED: ${provider}`
|
||||
}
|
||||
|
||||
function open(provider: string): string {
|
||||
if (!provider) {
|
||||
return "ERROR: No provider specified"
|
||||
}
|
||||
|
||||
KeybindsService.loadProvider(provider)
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
return `KEYBINDS_OPEN_SUCCESS: ${provider}`
|
||||
}
|
||||
return `KEYBINDS_OPEN_FAILED: ${provider}`
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
return "KEYBINDS_CLOSE_SUCCESS"
|
||||
}
|
||||
return "KEYBINDS_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
target: "keybinds"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function openBinds(): string {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
KeybindsService.loadProvider("hyprland")
|
||||
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"
|
||||
}
|
||||
KeybindsService.loadProvider("hyprland")
|
||||
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"
|
||||
}
|
||||
|
||||
function toggleOverview(): string {
|
||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
||||
return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function closeOverview(): string {
|
||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = false
|
||||
return "OVERVIEW_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function openOverview(): string {
|
||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = true
|
||||
return "OVERVIEW_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
target: "hypr"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function wallpaper(): string {
|
||||
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
|
||||
return "SUCCESS: Toggled wallpaper browser"
|
||||
}
|
||||
return "ERROR: Failed to toggle wallpaper browser"
|
||||
}
|
||||
|
||||
target: "dankdash"
|
||||
}
|
||||
}
|
||||
3
LICENSE
3
LICENSE
@@ -17,4 +17,5 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
156
Makefile
Normal file
156
Makefile
Normal file
@@ -0,0 +1,156 @@
|
||||
# Root Makefile for DankMaterialShell (DMS)
|
||||
# Orchestrates building, installation, and systemd management
|
||||
|
||||
# Build configuration
|
||||
BINARY_NAME=dms
|
||||
CORE_DIR=core
|
||||
BUILD_DIR=$(CORE_DIR)/bin
|
||||
PREFIX ?= /usr/local
|
||||
INSTALL_DIR=$(PREFIX)/bin
|
||||
DATA_DIR=$(PREFIX)/share
|
||||
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
||||
|
||||
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
||||
SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user
|
||||
|
||||
SHELL_DIR=quickshell
|
||||
SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||
ASSETS_DIR=assets
|
||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||
|
||||
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@$(MAKE) -C $(CORE_DIR) build
|
||||
@echo "Build complete"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@$(MAKE) -C $(CORE_DIR) clean
|
||||
@echo "Clean complete"
|
||||
|
||||
# Installation targets
|
||||
install-bin:
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Binary installed"
|
||||
|
||||
install-shell:
|
||||
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
||||
@echo "Shell files installed"
|
||||
|
||||
install-completions:
|
||||
@echo "Installing shell completions..."
|
||||
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
||||
@mkdir -p $(DATA_DIR)/zsh/site-functions
|
||||
@mkdir -p $(DATA_DIR)/fish/vendor_completions.d
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) completion bash > $(DATA_DIR)/bash-completion/completions/dms 2>/dev/null || true
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) completion zsh > $(DATA_DIR)/zsh/site-functions/_dms 2>/dev/null || true
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) completion fish > $(DATA_DIR)/fish/vendor_completions.d/dms.fish 2>/dev/null || true
|
||||
@echo "Shell completions installed"
|
||||
|
||||
install-systemd:
|
||||
@echo "Installing systemd user service..."
|
||||
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
|
||||
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||
|
||||
install-icon:
|
||||
@echo "Installing icon..."
|
||||
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg
|
||||
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||
@echo "Icon installed"
|
||||
|
||||
install-desktop:
|
||||
@echo "Installing desktop entry..."
|
||||
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||
@echo "Desktop entry installed"
|
||||
|
||||
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||
@echo ""
|
||||
@echo "Installation complete!"
|
||||
@echo ""
|
||||
@echo "=== Cheers, the DMS Team! ==="
|
||||
|
||||
# Uninstallation targets
|
||||
uninstall-bin:
|
||||
@echo "Removing $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Binary removed"
|
||||
|
||||
uninstall-shell:
|
||||
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
|
||||
@rm -rf $(SHELL_INSTALL_DIR)
|
||||
@echo "Shell files removed"
|
||||
|
||||
uninstall-completions:
|
||||
@echo "Removing shell completions..."
|
||||
@rm -f $(DATA_DIR)/bash-completion/completions/dms
|
||||
@rm -f $(DATA_DIR)/zsh/site-functions/_dms
|
||||
@rm -f $(DATA_DIR)/fish/vendor_completions.d/dms.fish
|
||||
@echo "Shell completions removed"
|
||||
|
||||
uninstall-systemd:
|
||||
@echo "Removing systemd user service..."
|
||||
@rm -f $(SYSTEMD_USER_DIR)/dms.service
|
||||
@echo "Systemd service removed"
|
||||
@echo "Note: Stop/disable service manually if running: systemctl --user stop dms"
|
||||
|
||||
uninstall-icon:
|
||||
@echo "Removing icon..."
|
||||
@rm -f $(ICON_DIR)/danklogo.svg
|
||||
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||
@echo "Icon removed"
|
||||
|
||||
uninstall-desktop:
|
||||
@echo "Removing desktop entry..."
|
||||
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||
@echo "Desktop entry removed"
|
||||
|
||||
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
|
||||
@echo ""
|
||||
@echo "Uninstallation complete!"
|
||||
|
||||
# Target assist
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo ""
|
||||
@echo "Build:"
|
||||
@echo " all (default) - Build the DMS binary"
|
||||
@echo " build - Same as 'all'"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo ""
|
||||
@echo "Install:"
|
||||
@echo " install - Build and install everything (requires sudo)"
|
||||
@echo " install-bin - Install only the binary"
|
||||
@echo " install-shell - Install only shell files"
|
||||
@echo " install-completions - Install only shell completions"
|
||||
@echo " install-systemd - Install only systemd service"
|
||||
@echo " install-icon - Install only icon"
|
||||
@echo " install-desktop - Install only desktop entry"
|
||||
@echo ""
|
||||
@echo "Uninstall:"
|
||||
@echo " uninstall - Remove everything (requires sudo)"
|
||||
@echo " uninstall-bin - Remove only the binary"
|
||||
@echo " uninstall-shell - Remove only shell files"
|
||||
@echo " uninstall-completions - Remove only shell completions"
|
||||
@echo " uninstall-systemd - Remove only systemd service"
|
||||
@echo " uninstall-icon - Remove only icon"
|
||||
@echo " uninstall-desktop - Remove only desktop entry"
|
||||
@echo ""
|
||||
@echo "Usage:"
|
||||
@echo " sudo make install - Build and install DMS"
|
||||
@echo " sudo make uninstall - Remove DMS"
|
||||
@echo " systemctl --user enable --now dms - Enable and start service"
|
||||
@@ -1,219 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: clipboardHistoryModal
|
||||
|
||||
layerNamespace: "dms:clipboard"
|
||||
|
||||
property int totalCount: 0
|
||||
property var clipboardEntries: []
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool showKeyboardHints: false
|
||||
property Component clipboardContent
|
||||
property int activeImageLoads: 0
|
||||
readonly property int maxConcurrentLoads: 3
|
||||
|
||||
function updateFilteredModel() {
|
||||
filteredClipboardModel.clear()
|
||||
for (var i = 0; i < clipboardModel.count; i++) {
|
||||
const entry = clipboardModel.get(i).entry
|
||||
if (searchText.trim().length === 0) {
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
})
|
||||
} else {
|
||||
const content = getEntryPreview(entry).toLowerCase()
|
||||
if (content.includes(searchText.toLowerCase())) {
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
||||
if (filteredClipboardModel.count === 0) {
|
||||
keyboardNavigationActive = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
||||
selectedIndex = filteredClipboardModel.count - 1
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
open()
|
||||
clipboardHistoryModal.searchText = ""
|
||||
clipboardHistoryModal.activeImageLoads = 0
|
||||
clipboardHistoryModal.shouldHaveFocus = true
|
||||
refreshClipboard()
|
||||
keyboardController.reset()
|
||||
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.text = ""
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
clipboardHistoryModal.searchText = ""
|
||||
clipboardHistoryModal.activeImageLoads = 0
|
||||
updateFilteredModel()
|
||||
keyboardController.reset()
|
||||
cleanupTempFiles()
|
||||
}
|
||||
|
||||
function cleanupTempFiles() {
|
||||
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
|
||||
}
|
||||
|
||||
function refreshClipboard() {
|
||||
clipboardProcesses.refresh()
|
||||
}
|
||||
|
||||
function copyEntry(entry) {
|
||||
const entryId = entry.split('\t')[0]
|
||||
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
|
||||
ToastService.showInfo(I18n.tr("Copied to clipboard"))
|
||||
hide()
|
||||
}
|
||||
|
||||
function deleteEntry(entry) {
|
||||
clipboardProcesses.deleteEntry(entry)
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
clipboardProcesses.clearAll()
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
let content = entry.replace(/^\s*\d+\s+/, "")
|
||||
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
||||
const dimensionMatch = content.match(/(\d+)x(\d+)/)
|
||||
if (dimensionMatch) {
|
||||
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
|
||||
}
|
||||
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
|
||||
if (typeMatch) {
|
||||
return `Image (${typeMatch[1].toUpperCase()})`
|
||||
}
|
||||
return "Image"
|
||||
}
|
||||
if (content.length > ClipboardConstants.previewLength) {
|
||||
return content.substring(0, ClipboardConstants.previewLength) + "..."
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
function getEntryType(entry) {
|
||||
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
|
||||
return "image"
|
||||
}
|
||||
if (entry.length > ClipboardConstants.longTextThreshold) {
|
||||
return "long_text"
|
||||
}
|
||||
return "text"
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: ClipboardConstants.modalWidth
|
||||
height: ClipboardConstants.modalHeight
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
enableShadow: true
|
||||
onBackgroundClicked: hide()
|
||||
modalFocusScope.Keys.onPressed: function (event) {
|
||||
keyboardController.handleKey(event)
|
||||
}
|
||||
content: clipboardContent
|
||||
|
||||
ClipboardKeyboardController {
|
||||
id: keyboardController
|
||||
modal: clipboardHistoryModal
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: clearConfirmDialog
|
||||
confirmButtonText: I18n.tr("Clear All")
|
||||
confirmButtonColor: Theme.primary
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
clipboardHistoryModal.shouldHaveFocus = false
|
||||
} else if (clipboardHistoryModal.shouldBeVisible) {
|
||||
clipboardHistoryModal.shouldHaveFocus = true
|
||||
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
|
||||
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
|
||||
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property alias filteredClipboardModel: filteredClipboardModel
|
||||
property alias clipboardModel: clipboardModel
|
||||
property var confirmDialog: clearConfirmDialog
|
||||
|
||||
ListModel {
|
||||
id: clipboardModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredClipboardModel
|
||||
}
|
||||
|
||||
ClipboardProcesses {
|
||||
id: clipboardProcesses
|
||||
modal: clipboardHistoryModal
|
||||
clipboardModel: clipboardModel
|
||||
filteredClipboardModel: filteredClipboardModel
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
clipboardHistoryModal.show()
|
||||
return "CLIPBOARD_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
clipboardHistoryModal.hide()
|
||||
return "CLIPBOARD_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
clipboardHistoryModal.toggle()
|
||||
return "CLIPBOARD_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "clipboard"
|
||||
}
|
||||
|
||||
clipboardContent: Component {
|
||||
ClipboardContent {
|
||||
modal: clipboardHistoryModal
|
||||
filteredModel: filteredClipboardModel
|
||||
clearConfirmDialog: clipboardHistoryModal.confirmDialog
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
QtObject {
|
||||
id: keyboardController
|
||||
|
||||
required property var modal
|
||||
|
||||
function reset() {
|
||||
modal.selectedIndex = 0
|
||||
modal.keyboardNavigationActive = false
|
||||
modal.showKeyboardHints = false
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
|
||||
return
|
||||
}
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.filteredClipboardModel.count - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
|
||||
return
|
||||
}
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0)
|
||||
}
|
||||
|
||||
function copySelected() {
|
||||
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
|
||||
return
|
||||
}
|
||||
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
|
||||
modal.copyEntry(selectedEntry)
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
|
||||
return
|
||||
}
|
||||
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
|
||||
modal.deleteEntry(selectedEntry)
|
||||
}
|
||||
|
||||
function handleKey(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = false
|
||||
event.accepted = true
|
||||
} else {
|
||||
modal.hide()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
event.accepted = true
|
||||
} else {
|
||||
selectNext()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Up || event.key === Qt.Key_Backtab) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
event.accepted = true
|
||||
} else if (modal.selectedIndex === 0) {
|
||||
modal.keyboardNavigationActive = false
|
||||
event.accepted = true
|
||||
} else {
|
||||
selectPrevious()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
} else {
|
||||
selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
} else if (modal.selectedIndex === 0) {
|
||||
modal.keyboardNavigationActive = false
|
||||
} else {
|
||||
selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
} else {
|
||||
selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
} else if (modal.selectedIndex === 0) {
|
||||
modal.keyboardNavigationActive = false
|
||||
} else {
|
||||
selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Delete && (event.modifiers & Qt.ShiftModifier)) {
|
||||
modal.clearAll()
|
||||
modal.hide()
|
||||
event.accepted = true
|
||||
} else if (modal.keyboardNavigationActive) {
|
||||
if ((event.key === Qt.Key_C && (event.modifiers & Qt.ControlModifier)) || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
copySelected()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Delete) {
|
||||
deleteSelected()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
if (event.key === Qt.Key_F10) {
|
||||
modal.showKeyboardHints = !modal.showKeyboardHints
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
|
||||
QtObject {
|
||||
id: clipboardProcesses
|
||||
|
||||
required property var modal
|
||||
required property var clipboardModel
|
||||
required property var filteredClipboardModel
|
||||
|
||||
// Load clipboard entries
|
||||
property var loadProcess: Process {
|
||||
id: loadProcess
|
||||
command: ["cliphist", "list"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
clipboardModel.clear()
|
||||
const lines = text.trim().split('\n')
|
||||
for (const line of lines) {
|
||||
if (line.trim().length > 0) {
|
||||
clipboardModel.append({
|
||||
"entry": line
|
||||
})
|
||||
}
|
||||
}
|
||||
modal.updateFilteredModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete single entry
|
||||
property var deleteProcess: Process {
|
||||
id: deleteProcess
|
||||
property string deletedEntry: ""
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
for (var i = 0; i < clipboardModel.count; i++) {
|
||||
if (clipboardModel.get(i).entry === deleteProcess.deletedEntry) {
|
||||
clipboardModel.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
for (var j = 0; j < filteredClipboardModel.count; j++) {
|
||||
if (filteredClipboardModel.get(j).entry === deleteProcess.deletedEntry) {
|
||||
filteredClipboardModel.remove(j)
|
||||
break
|
||||
}
|
||||
}
|
||||
modal.totalCount = filteredClipboardModel.count
|
||||
if (filteredClipboardModel.count === 0) {
|
||||
modal.keyboardNavigationActive = false
|
||||
modal.selectedIndex = 0
|
||||
} else if (modal.selectedIndex >= filteredClipboardModel.count) {
|
||||
modal.selectedIndex = filteredClipboardModel.count - 1
|
||||
}
|
||||
} else {
|
||||
console.warn("Failed to delete clipboard entry")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all entries
|
||||
property var clearProcess: Process {
|
||||
id: clearProcess
|
||||
command: ["cliphist", "wipe"]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
clipboardModel.clear()
|
||||
filteredClipboardModel.clear()
|
||||
modal.totalCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
loadProcess.running = true
|
||||
}
|
||||
|
||||
function deleteEntry(entry) {
|
||||
deleteProcess.deletedEntry = entry
|
||||
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`]
|
||||
deleteProcess.running = true
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
clearProcess.running = true
|
||||
}
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property string layerNamespace: "dms:modal"
|
||||
WlrLayershell.namespace: layerNamespace
|
||||
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property Item directContent: null
|
||||
property real width: 400
|
||||
property real height: 300
|
||||
readonly property real screenWidth: screen ? screen.width : 1920
|
||||
readonly property real screenHeight: screen ? screen.height : 1080
|
||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
||||
property bool showBackground: true
|
||||
property real backgroundOpacity: 0.5
|
||||
property string positioning: "center"
|
||||
property point customPosition: Qt.point(0, 0)
|
||||
property bool closeOnEscapeKey: true
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 1
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
property bool enableShadow: false
|
||||
property alias modalFocusScope: focusScope
|
||||
property bool shouldBeVisible: false
|
||||
property bool shouldHaveFocus: shouldBeVisible
|
||||
property bool allowFocusOverride: false
|
||||
property bool allowStacking: false
|
||||
property bool keepContentLoaded: false
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
signal backgroundClicked
|
||||
|
||||
function open() {
|
||||
ModalManager.openModal(root)
|
||||
closeTimer.stop()
|
||||
shouldBeVisible = true
|
||||
visible = true
|
||||
shouldHaveFocus = false
|
||||
Qt.callLater(() => {
|
||||
shouldHaveFocus = Qt.binding(() => shouldBeVisible)
|
||||
})
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false
|
||||
shouldHaveFocus = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
close()
|
||||
} else {
|
||||
open()
|
||||
}
|
||||
}
|
||||
|
||||
visible: shouldBeVisible
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
return WlrLayershell.Bottom
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay
|
||||
case "background":
|
||||
return WlrLayershell.Background
|
||||
default:
|
||||
return WlrLayershell.Top
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
onVisibleChanged: {
|
||||
if (root.visible) {
|
||||
opened()
|
||||
} else {
|
||||
if (Qt.inputMethod) {
|
||||
Qt.inputMethod.hide()
|
||||
Qt.inputMethod.reset()
|
||||
}
|
||||
dialogClosed()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
target: ModalManager
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
|
||||
interval: animationDuration + 120
|
||||
onTriggered: {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
onClicked: mouse => {
|
||||
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
||||
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
||||
root.backgroundClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.showBackground && SettingsData.modalDarkenBackground
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
|
||||
width: Theme.px(root.width, dpr)
|
||||
height: Theme.px(root.height, dpr)
|
||||
x: {
|
||||
if (positioning === "center") {
|
||||
return Theme.snap((root.screenWidth - width) / 2, dpr)
|
||||
} else if (positioning === "top-right") {
|
||||
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
|
||||
} else if (positioning === "custom") {
|
||||
return Theme.snap(root.customPosition.x, dpr)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
y: {
|
||||
if (positioning === "center") {
|
||||
return Theme.snap((root.screenHeight - height) / 2, dpr)
|
||||
} else if (positioning === "top-right") {
|
||||
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
|
||||
} else if (positioning === "custom") {
|
||||
return Theme.snap(root.customPosition.y, dpr)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr)
|
||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr)
|
||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
clip: false
|
||||
layer.enabled: true
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(modalContainer.animY + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
radius: root.cornerRadius
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onDirectContentChanged() {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
|
||||
objectName: "modalFocusScope"
|
||||
anchors.fill: parent
|
||||
visible: root.shouldBeVisible || root.visible
|
||||
focus: root.shouldBeVisible
|
||||
Keys.onEscapePressed: event => {
|
||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,934 +0,0 @@
|
||||
import Qt.labs.folderlistmodel
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: fileBrowserModal
|
||||
|
||||
layerNamespace: "dms:file-browser"
|
||||
|
||||
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
|
||||
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
|
||||
property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation)
|
||||
property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation)
|
||||
property string currentPath: ""
|
||||
property var fileExtensions: ["*.*"]
|
||||
property alias filterExtensions: fileBrowserModal.fileExtensions
|
||||
property string browserTitle: "Select File"
|
||||
property string browserIcon: "folder_open"
|
||||
property string browserType: "generic"
|
||||
property bool showHiddenFiles: false
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool backButtonFocused: false
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property int keyboardSelectionIndex: -1
|
||||
property bool keyboardSelectionRequested: false
|
||||
property bool showKeyboardHints: false
|
||||
property bool showFileInfo: false
|
||||
property string selectedFilePath: ""
|
||||
property string selectedFileName: ""
|
||||
property bool selectedFileIsDir: false
|
||||
property bool showOverwriteConfirmation: false
|
||||
property string pendingFilePath: ""
|
||||
property var parentModal: null
|
||||
property bool showSidebar: true
|
||||
property string viewMode: "grid"
|
||||
property string sortBy: "name"
|
||||
property bool sortAscending: true
|
||||
property int iconSizeIndex: 1
|
||||
property var iconSizes: [80, 120, 160, 200]
|
||||
property bool pathEditMode: false
|
||||
property bool pathInputHasFocus: false
|
||||
property int actualGridColumns: 5
|
||||
property bool _initialized: false
|
||||
|
||||
signal fileSelected(string path)
|
||||
|
||||
function loadSettings() {
|
||||
const type = browserType || "default"
|
||||
const settings = CacheData.fileBrowserSettings[type]
|
||||
const isImageBrowser = ["wallpaper", "profile"].includes(browserType)
|
||||
|
||||
if (settings) {
|
||||
viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list")
|
||||
sortBy = settings.sortBy || "name"
|
||||
sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true
|
||||
iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1
|
||||
showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true
|
||||
} else {
|
||||
viewMode = isImageBrowser ? "grid" : "list"
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!_initialized)
|
||||
return
|
||||
|
||||
const type = browserType || "default"
|
||||
let settings = CacheData.fileBrowserSettings
|
||||
if (!settings[type]) {
|
||||
settings[type] = {}
|
||||
}
|
||||
settings[type].viewMode = viewMode
|
||||
settings[type].sortBy = sortBy
|
||||
settings[type].sortAscending = sortAscending
|
||||
settings[type].iconSizeIndex = iconSizeIndex
|
||||
settings[type].showSidebar = showSidebar
|
||||
settings[type].lastPath = currentPath
|
||||
CacheData.fileBrowserSettings = settings
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = currentPath
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = currentPath
|
||||
}
|
||||
|
||||
CacheData.saveCache()
|
||||
}
|
||||
|
||||
onViewModeChanged: saveSettings()
|
||||
onSortByChanged: saveSettings()
|
||||
onSortAscendingChanged: saveSettings()
|
||||
onIconSizeIndexChanged: saveSettings()
|
||||
onShowSidebarChanged: saveSettings()
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false
|
||||
}
|
||||
const ext = fileName.toLowerCase().split('.').pop()
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
const type = browserType || "default"
|
||||
const settings = CacheData.fileBrowserSettings[type]
|
||||
const lastPath = settings?.lastPath || ""
|
||||
return (lastPath && lastPath !== "") ? lastPath : homeDir
|
||||
}
|
||||
|
||||
function saveLastPath(path) {
|
||||
const type = browserType || "default"
|
||||
let settings = CacheData.fileBrowserSettings
|
||||
if (!settings[type]) {
|
||||
settings[type] = {}
|
||||
}
|
||||
settings[type].lastPath = path
|
||||
CacheData.fileBrowserSettings = settings
|
||||
CacheData.saveCache()
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = path
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = path
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedFileData(path, name, isDir) {
|
||||
selectedFilePath = path
|
||||
selectedFileName = name
|
||||
selectedFileIsDir = isDir
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
const path = currentPath
|
||||
if (path === homeDir)
|
||||
return
|
||||
|
||||
const lastSlash = path.lastIndexOf('/')
|
||||
if (lastSlash > 0) {
|
||||
const newPath = path.substring(0, lastSlash)
|
||||
if (newPath.length < homeDir.length) {
|
||||
currentPath = homeDir
|
||||
saveLastPath(homeDir)
|
||||
} else {
|
||||
currentPath = newPath
|
||||
saveLastPath(newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(path) {
|
||||
currentPath = path
|
||||
saveLastPath(path)
|
||||
selectedIndex = -1
|
||||
backButtonFocused = false
|
||||
}
|
||||
|
||||
function keyboardFileSelection(index) {
|
||||
if (index >= 0) {
|
||||
keyboardSelectionTimer.targetIndex = index
|
||||
keyboardSelectionTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
function executeKeyboardSelection(index) {
|
||||
keyboardSelectionIndex = index
|
||||
keyboardSelectionRequested = true
|
||||
}
|
||||
|
||||
function handleSaveFile(filePath) {
|
||||
var normalizedPath = filePath
|
||||
if (!normalizedPath.startsWith("file://")) {
|
||||
normalizedPath = "file://" + filePath
|
||||
}
|
||||
|
||||
var exists = false
|
||||
var fileName = filePath.split('/').pop()
|
||||
|
||||
for (var i = 0; i < folderModel.count; i++) {
|
||||
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
pendingFilePath = normalizedPath
|
||||
showOverwriteConfirmation = true
|
||||
} else {
|
||||
fileSelected(normalizedPath)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
|
||||
objectName: "fileBrowserModal"
|
||||
allowStacking: true
|
||||
closeOnEscapeKey: false
|
||||
shouldHaveFocus: shouldBeVisible
|
||||
Component.onCompleted: {
|
||||
loadSettings()
|
||||
currentPath = getLastPath()
|
||||
_initialized = true
|
||||
}
|
||||
|
||||
property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
|
||||
property int currentPathIndex: 0
|
||||
|
||||
width: 800
|
||||
height: 600
|
||||
enableShadow: true
|
||||
visible: false
|
||||
onBackgroundClicked: close()
|
||||
onOpened: {
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = false
|
||||
parentModal.allowFocusOverride = true
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader && contentLoader.item) {
|
||||
contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
onDialogClosed: {
|
||||
if (parentModal) {
|
||||
parentModal.allowFocusOverride = false
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
currentPath = getLastPath()
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
backButtonFocused = false
|
||||
}
|
||||
}
|
||||
onCurrentPathChanged: {
|
||||
selectedFilePath = ""
|
||||
selectedFileName = ""
|
||||
selectedFileIsDir = false
|
||||
saveSettings()
|
||||
}
|
||||
onSelectedIndexChanged: {
|
||||
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
||||
selectedFilePath = ""
|
||||
selectedFileName = ""
|
||||
selectedFileIsDir = false
|
||||
}
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
|
||||
showDirsFirst: true
|
||||
showDotAndDotDot: false
|
||||
showHidden: fileBrowserModal.showHiddenFiles
|
||||
nameFilters: fileExtensions
|
||||
showFiles: true
|
||||
showDirs: true
|
||||
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
|
||||
sortField: {
|
||||
switch (sortBy) {
|
||||
case "name":
|
||||
return FolderListModel.Name
|
||||
case "size":
|
||||
return FolderListModel.Size
|
||||
case "modified":
|
||||
return FolderListModel.Time
|
||||
case "type":
|
||||
return FolderListModel.Type
|
||||
default:
|
||||
return FolderListModel.Name
|
||||
}
|
||||
}
|
||||
sortReversed: !sortAscending
|
||||
}
|
||||
|
||||
property var quickAccessLocations: [{
|
||||
"name": "Home",
|
||||
"path": homeDir,
|
||||
"icon": "home"
|
||||
}, {
|
||||
"name": "Documents",
|
||||
"path": docsDir,
|
||||
"icon": "description"
|
||||
}, {
|
||||
"name": "Downloads",
|
||||
"path": downloadDir,
|
||||
"icon": "download"
|
||||
}, {
|
||||
"name": "Pictures",
|
||||
"path": picsDir,
|
||||
"icon": "image"
|
||||
}, {
|
||||
"name": "Music",
|
||||
"path": musicDir,
|
||||
"icon": "music_note"
|
||||
}, {
|
||||
"name": "Videos",
|
||||
"path": videosDir,
|
||||
"icon": "movie"
|
||||
}, {
|
||||
"name": "Desktop",
|
||||
"path": desktopDir,
|
||||
"icon": "computer"
|
||||
}]
|
||||
|
||||
QtObject {
|
||||
id: keyboardController
|
||||
|
||||
property int totalItems: folderModel.count
|
||||
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
|
||||
|
||||
function handleKey(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
close()
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
if (event.key === Qt.Key_F10) {
|
||||
showKeyboardHints = !showKeyboardHints
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
||||
showFileInfo = !showFileInfo
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
||||
if (currentPath !== homeDir) {
|
||||
navigateUp()
|
||||
event.accepted = true
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!keyboardNavigationActive) {
|
||||
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key
|
||||
=== Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier)
|
||||
|
||||
if (isInitKey) {
|
||||
keyboardNavigationActive = true
|
||||
if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
} else {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
return
|
||||
}
|
||||
switch (event.key) {
|
||||
case Qt.Key_Tab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
} else {
|
||||
selectedIndex = 0
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Backtab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = totalItems - 1
|
||||
} else if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
} else {
|
||||
selectedIndex = totalItems - 1
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_P:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_H:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (!backButtonFocused && selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_L:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_Left:
|
||||
if (pathInputHasFocus)
|
||||
return
|
||||
if (backButtonFocused)
|
||||
return
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Right:
|
||||
if (pathInputHasFocus)
|
||||
return
|
||||
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Up:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
if (gridColumns === 1) {
|
||||
selectedIndex = 0
|
||||
} else {
|
||||
var col = selectedIndex % gridColumns
|
||||
selectedIndex = Math.min(col, totalItems - 1)
|
||||
}
|
||||
} else if (selectedIndex >= gridColumns) {
|
||||
selectedIndex -= gridColumns
|
||||
} else if (selectedIndex > 0 && gridColumns === 1) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Down:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (gridColumns === 1) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
} else {
|
||||
var newIndex = selectedIndex + gridColumns
|
||||
if (newIndex < totalItems) {
|
||||
selectedIndex = newIndex
|
||||
} else {
|
||||
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
|
||||
var col = selectedIndex % gridColumns
|
||||
var targetIndex = lastRowStart + col
|
||||
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
||||
selectedIndex = targetIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
case Qt.Key_Space:
|
||||
if (backButtonFocused)
|
||||
navigateUp()
|
||||
else if (selectedIndex >= 0 && selectedIndex < totalItems)
|
||||
fileBrowserModal.keyboardFileSelection(selectedIndex)
|
||||
event.accepted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: keyboardSelectionTimer
|
||||
|
||||
property int targetIndex: -1
|
||||
|
||||
interval: 1
|
||||
onTriggered: {
|
||||
executeKeyboardSelection(targetIndex)
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onPressed: event => {
|
||||
keyboardController.handleKey(event)
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
|
||||
DankIcon {
|
||||
name: browserIcon
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: browserTitle
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: showHiddenFiles ? "visibility_off" : "visibility"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
|
||||
onClicked: showHiddenFiles = !showHiddenFiles
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: viewMode === "grid" ? "view_list" : "grid_view"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
visible: viewMode === "grid"
|
||||
onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 49
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Row {
|
||||
width: showSidebar ? 201 : 0
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
visible: showSidebar
|
||||
|
||||
FileBrowserSidebar {
|
||||
height: parent.height
|
||||
quickAccessLocations: fileBrowserModal.quickAccessLocations
|
||||
currentPath: fileBrowserModal.currentPath
|
||||
onLocationSelected: path => navigateTo(path)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 1
|
||||
height: parent.height
|
||||
color: Theme.outline
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - (showSidebar ? 201 : 0)
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
|
||||
FileBrowserNavigation {
|
||||
width: parent.width
|
||||
currentPath: fileBrowserModal.currentPath
|
||||
homeDir: fileBrowserModal.homeDir
|
||||
backButtonFocused: fileBrowserModal.backButtonFocused
|
||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
||||
showSidebar: fileBrowserModal.showSidebar
|
||||
pathEditMode: fileBrowserModal.pathEditMode
|
||||
onNavigateUp: fileBrowserModal.navigateUp()
|
||||
onNavigateTo: path => fileBrowserModal.navigateTo(path)
|
||||
onPathInputFocusChanged: hasFocus => {
|
||||
fileBrowserModal.pathInputHasFocus = hasFocus
|
||||
if (hasFocus) {
|
||||
fileBrowserModal.pathEditMode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gridContainer
|
||||
width: parent.width
|
||||
height: parent.height - 41
|
||||
clip: true
|
||||
|
||||
property real gridCellWidth: iconSizes[iconSizeIndex] + 24
|
||||
property real gridCellHeight: iconSizes[iconSizeIndex] + 56
|
||||
property real availableGridWidth: width - Theme.spacingM * 2
|
||||
property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
|
||||
property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
|
||||
|
||||
onGridColumnsChanged: {
|
||||
fileBrowserModal.actualGridColumns = gridColumns
|
||||
}
|
||||
Component.onCompleted: {
|
||||
fileBrowserModal.actualGridColumns = gridColumns
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: fileGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: gridContainer.gridLeftMargin
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "grid"
|
||||
cellWidth: gridContainer.gridCellWidth
|
||||
cellHeight: gridContainer.gridCellHeight
|
||||
cacheBuffer: 260
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, GridView.Contain)
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: gridScrollbar
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: FileBrowserGridDelegate {
|
||||
iconSizes: fileBrowserModal.iconSizes
|
||||
iconSizeIndex: fileBrowserModal.iconSizeIndex
|
||||
selectedIndex: fileBrowserModal.selectedIndex
|
||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index
|
||||
setSelectedFileData(path, name, isDir)
|
||||
if (isDir) {
|
||||
navigateTo(path)
|
||||
} else {
|
||||
fileSelected(path)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir)
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
|
||||
fileBrowserModal.keyboardSelectionRequested = false
|
||||
selectedIndex = index
|
||||
setSelectedFileData(filePath, fileName, fileIsDir)
|
||||
if (fileIsDir) {
|
||||
navigateTo(filePath)
|
||||
} else {
|
||||
fileSelected(filePath)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target: fileBrowserModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: fileList
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "list"
|
||||
spacing: 2
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: listScrollbar
|
||||
}
|
||||
|
||||
delegate: FileBrowserListDelegate {
|
||||
width: fileList.width
|
||||
selectedIndex: fileBrowserModal.selectedIndex
|
||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index
|
||||
setSelectedFileData(path, name, isDir)
|
||||
if (isDir) {
|
||||
navigateTo(path)
|
||||
} else {
|
||||
fileSelected(path)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir)
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
|
||||
fileBrowserModal.keyboardSelectionRequested = false
|
||||
selectedIndex = index
|
||||
setSelectedFileData(filePath, fileName, fileIsDir)
|
||||
if (fileIsDir) {
|
||||
navigateTo(filePath)
|
||||
} else {
|
||||
fileSelected(filePath)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target: fileBrowserModal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSaveRow {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
saveMode: fileBrowserModal.saveMode
|
||||
defaultFileName: fileBrowserModal.defaultFileName
|
||||
currentPath: fileBrowserModal.currentPath
|
||||
onSaveRequested: filePath => handleSaveFile(filePath)
|
||||
}
|
||||
|
||||
KeyboardHints {
|
||||
id: keyboardHints
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
showHints: fileBrowserModal.showKeyboardHints
|
||||
}
|
||||
|
||||
FileInfo {
|
||||
id: fileInfo
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
width: 300
|
||||
showFileInfo: fileBrowserModal.showFileInfo
|
||||
selectedIndex: fileBrowserModal.selectedIndex
|
||||
sourceFolderModel: folderModel
|
||||
currentPath: fileBrowserModal.currentPath
|
||||
currentFileName: fileBrowserModal.selectedFileName
|
||||
currentFileIsDir: fileBrowserModal.selectedFileIsDir
|
||||
currentFileExtension: {
|
||||
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName)
|
||||
return ""
|
||||
|
||||
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
|
||||
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSortMenu {
|
||||
id: sortMenu
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 120
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
sortBy: fileBrowserModal.sortBy
|
||||
sortAscending: fileBrowserModal.sortAscending
|
||||
onSortBySelected: value => {
|
||||
fileBrowserModal.sortBy = value
|
||||
}
|
||||
onSortOrderSelected: ascending => {
|
||||
fileBrowserModal.sortAscending = ascending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserOverwriteDialog {
|
||||
anchors.fill: parent
|
||||
showDialog: showOverwriteConfirmation
|
||||
pendingFilePath: fileBrowserModal.pendingFilePath
|
||||
onConfirmed: filePath => {
|
||||
showOverwriteConfirmation = false
|
||||
fileSelected(filePath)
|
||||
pendingFilePath = ""
|
||||
Qt.callLater(() => fileBrowserModal.close())
|
||||
}
|
||||
onCancelled: {
|
||||
showOverwriteConfirmation = false
|
||||
pendingFilePath = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:keybinds"
|
||||
property real scrollStep: 60
|
||||
property var activeFlickable: null
|
||||
property real _maxW: Math.min(Screen.width * 0.92, 1200)
|
||||
property real _maxH: Math.min(Screen.height * 0.92, 900)
|
||||
width: _maxW
|
||||
height: _maxH
|
||||
onBackgroundClicked: close()
|
||||
|
||||
function scrollDown() {
|
||||
if (!root.activeFlickable) return
|
||||
let newY = root.activeFlickable.contentY + scrollStep
|
||||
newY = Math.min(newY, root.activeFlickable.contentHeight - root.activeFlickable.height)
|
||||
root.activeFlickable.contentY = newY
|
||||
}
|
||||
|
||||
function scrollUp() {
|
||||
if (!root.activeFlickable) return
|
||||
let newY = root.activeFlickable.contentY - root.scrollStep
|
||||
newY = Math.max(0, newY)
|
||||
root.activeFlickable.contentY = newY
|
||||
}
|
||||
|
||||
Shortcut { sequence: "Ctrl+j"; onActivated: root.scrollDown() }
|
||||
Shortcut { sequence: "Down"; onActivated: root.scrollDown() }
|
||||
Shortcut { sequence: "Ctrl+k"; onActivated: root.scrollUp() }
|
||||
Shortcut { sequence: "Up"; onActivated: root.scrollUp() }
|
||||
Shortcut { sequence: "Esc"; onActivated: root.close() }
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
StyledText {
|
||||
text: KeybindsService.keybinds.title || "Keybinds"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: mainFlickable
|
||||
width: parent.width
|
||||
height: parent.height - parent.spacing - 40
|
||||
contentWidth: rowLayout.implicitWidth
|
||||
contentHeight: rowLayout.implicitHeight
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: root.activeFlickable = mainFlickable
|
||||
|
||||
property var rawBinds: KeybindsService.keybinds.binds || {}
|
||||
property var categories: {
|
||||
const processed = {}
|
||||
for (const cat in rawBinds) {
|
||||
const binds = rawBinds[cat]
|
||||
const subcats = {}
|
||||
let hasSubcats = false
|
||||
|
||||
for (let i = 0; i < binds.length; i++) {
|
||||
const bind = binds[i]
|
||||
if (bind.subcat) {
|
||||
hasSubcats = true
|
||||
if (!subcats[bind.subcat]) {
|
||||
subcats[bind.subcat] = []
|
||||
}
|
||||
subcats[bind.subcat].push(bind)
|
||||
} else {
|
||||
if (!subcats["_root"]) {
|
||||
subcats["_root"] = []
|
||||
}
|
||||
subcats["_root"].push(bind)
|
||||
}
|
||||
}
|
||||
|
||||
processed[cat] = {
|
||||
hasSubcats: hasSubcats,
|
||||
subcats: subcats,
|
||||
subcatKeys: Object.keys(subcats)
|
||||
}
|
||||
}
|
||||
return processed
|
||||
}
|
||||
property var categoryKeys: Object.keys(categories)
|
||||
|
||||
function distributeCategories(cols) {
|
||||
const columns = []
|
||||
for (let i = 0; i < cols; i++) {
|
||||
columns.push([])
|
||||
}
|
||||
for (let i = 0; i < categoryKeys.length; i++) {
|
||||
columns[i % cols].push(categoryKeys[i])
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rowLayout
|
||||
width: mainFlickable.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
|
||||
property var columnCategories: mainFlickable.distributeCategories(numColumns)
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.numColumns
|
||||
|
||||
Column {
|
||||
id: masonryColumn
|
||||
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.columnCategories[index] || []
|
||||
|
||||
Column {
|
||||
id: categoryColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
property string catName: modelData
|
||||
property var catData: mainFlickable.categories[catName]
|
||||
|
||||
StyledText {
|
||||
text: categoryColumn.catName
|
||||
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.spacingM
|
||||
|
||||
Repeater {
|
||||
model: categoryColumn.catData?.subcatKeys || []
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
property string subcatName: modelData
|
||||
property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
|
||||
|
||||
StyledText {
|
||||
visible: parent.subcatName !== "_root"
|
||||
text: parent.subcatName
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.primary
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: parent.parent.subcatBinds
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: Math.min(140, parent.width * 0.42)
|
||||
height: 22
|
||||
radius: 4
|
||||
opacity: 0.9
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: 2
|
||||
width: parent.width - 4
|
||||
color: Theme.secondary
|
||||
text: modelData.key || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 150
|
||||
text: modelData.desc || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:polkit"
|
||||
|
||||
property string passwordInput: ""
|
||||
property var currentFlow: PolkitService.agent?.flow
|
||||
property bool isLoading: false
|
||||
property real minHeight: 240
|
||||
|
||||
function show() {
|
||||
passwordInput = ""
|
||||
isLoading = false
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.passwordField) {
|
||||
contentLoader.item.passwordField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
width: 420
|
||||
height: Math.max(minHeight, contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240)
|
||||
|
||||
Connections {
|
||||
target: contentLoader.item
|
||||
function onImplicitHeightChanged() {
|
||||
if (shouldBeVisible && contentLoader.item) {
|
||||
const newHeight = contentLoader.item.implicitHeight + Theme.spacingM * 2
|
||||
if (newHeight > minHeight) {
|
||||
minHeight = newHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.passwordField) {
|
||||
contentLoader.item.passwordField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
passwordInput = ""
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
onBackgroundClicked: () => {
|
||||
if (currentFlow && !isLoading) {
|
||||
currentFlow.cancelAuthenticationRequest()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PolkitService.agent
|
||||
enabled: PolkitService.polkitAvailable
|
||||
|
||||
function onAuthenticationRequestStarted() {
|
||||
show()
|
||||
}
|
||||
|
||||
function onIsActiveChanged() {
|
||||
if (!(PolkitService.agent?.isActive ?? false)) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: currentFlow
|
||||
enabled: currentFlow !== null
|
||||
|
||||
function onIsResponseRequiredChanged() {
|
||||
if (currentFlow.isResponseRequired) {
|
||||
isLoading = false
|
||||
passwordInput = ""
|
||||
if (contentLoader.item && contentLoader.item.passwordField) {
|
||||
contentLoader.item.passwordField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthenticationSucceeded() {
|
||||
close()
|
||||
}
|
||||
|
||||
function onAuthenticationFailed() {
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
function onAuthenticationRequestCancelled() {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: authContent
|
||||
|
||||
property alias passwordField: passwordField
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
implicitHeight: headerRow.implicitHeight + mainColumn.implicitHeight + Theme.spacingM
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
if (currentFlow && !isLoading) {
|
||||
currentFlow.cancelAuthenticationRequest()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Authentication Required")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: currentFlow?.message ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: (currentFlow?.supplementaryMessage ?? "") !== ""
|
||||
text: currentFlow?.supplementaryMessage ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: (currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
opacity: (currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
enabled: !isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onClicked: () => {
|
||||
if (currentFlow) {
|
||||
currentFlow.cancelAuthenticationRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: currentFlow?.inputPrompt ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
visible: (currentFlow?.inputPrompt ?? "") !== ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: passwordField.activeFocus ? 2 : 1
|
||||
opacity: isLoading ? 0.5 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !isLoading
|
||||
onClicked: () => {
|
||||
passwordField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordField
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: passwordInput
|
||||
echoMode: (currentFlow?.responseVisible ?? false) ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: ""
|
||||
backgroundColor: "transparent"
|
||||
enabled: !isLoading
|
||||
onTextEdited: () => {
|
||||
passwordInput = text
|
||||
}
|
||||
onAccepted: () => {
|
||||
if (passwordInput.length > 0 && currentFlow && !isLoading) {
|
||||
isLoading = true
|
||||
currentFlow.submit(passwordInput)
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: (currentFlow?.failed ?? false) ? failedText.implicitHeight : 0
|
||||
visible: height > 0
|
||||
|
||||
StyledText {
|
||||
id: failedText
|
||||
text: I18n.tr("Authentication failed, please try again")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
width: parent.width
|
||||
opacity: (currentFlow?.failed ?? false) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
enabled: !isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: cancelText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: () => {
|
||||
if (currentFlow) {
|
||||
currentFlow.cancelAuthenticationRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, authText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: authArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: !isLoading && (passwordInput.length > 0 || !(currentFlow?.isResponseRequired ?? true))
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: authText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Authenticate")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: authArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: () => {
|
||||
if (currentFlow && !isLoading) {
|
||||
isLoading = true
|
||||
currentFlow.submit(passwordInput)
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:power-menu"
|
||||
|
||||
property int selectedRow: 0
|
||||
property int selectedCol: 0
|
||||
property int selectedIndex: selectedRow * gridColumns + selectedCol
|
||||
property rect parentBounds: Qt.rect(0, 0, 0, 0)
|
||||
property var parentScreen: null
|
||||
property var visibleActions: []
|
||||
property int gridColumns: 3
|
||||
property int gridRows: 2
|
||||
|
||||
signal powerActionRequested(string action, string title, string message)
|
||||
signal lockRequested
|
||||
|
||||
function openCentered() {
|
||||
parentBounds = Qt.rect(0, 0, 0, 0)
|
||||
parentScreen = null
|
||||
backgroundOpacity = 0.5
|
||||
open()
|
||||
}
|
||||
|
||||
function openFromControlCenter(bounds, targetScreen) {
|
||||
parentBounds = bounds
|
||||
parentScreen = targetScreen
|
||||
backgroundOpacity = 0
|
||||
open()
|
||||
}
|
||||
|
||||
function updateVisibleActions() {
|
||||
const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||
visibleActions = allActions.filter(action => {
|
||||
if (action === "hibernate" && !SessionService.hibernateSupported)
|
||||
return false
|
||||
return true
|
||||
})
|
||||
|
||||
const count = visibleActions.length
|
||||
switch (count) {
|
||||
case 0:
|
||||
gridColumns = 1
|
||||
gridRows = 1
|
||||
break
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
gridColumns = 1
|
||||
gridRows = count
|
||||
break
|
||||
case 4:
|
||||
gridColumns = 2
|
||||
gridRows = 2
|
||||
break
|
||||
default:
|
||||
gridColumns = 3
|
||||
gridRows = Math.ceil(count / 3)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultActionIndex() {
|
||||
const defaultAction = SettingsData.powerMenuDefaultAction || "logout"
|
||||
const index = visibleActions.indexOf(defaultAction)
|
||||
return index >= 0 ? index : 0
|
||||
}
|
||||
|
||||
function getActionAtIndex(index) {
|
||||
if (index < 0 || index >= visibleActions.length)
|
||||
return ""
|
||||
return visibleActions[index]
|
||||
}
|
||||
|
||||
function getActionData(action) {
|
||||
switch (action) {
|
||||
case "reboot":
|
||||
return {
|
||||
"icon": "restart_alt",
|
||||
"label": I18n.tr("Reboot"),
|
||||
"key": "R"
|
||||
}
|
||||
case "logout":
|
||||
return {
|
||||
"icon": "logout",
|
||||
"label": I18n.tr("Log Out"),
|
||||
"key": "X"
|
||||
}
|
||||
case "poweroff":
|
||||
return {
|
||||
"icon": "power_settings_new",
|
||||
"label": I18n.tr("Power Off"),
|
||||
"key": "P"
|
||||
}
|
||||
case "lock":
|
||||
return {
|
||||
"icon": "lock",
|
||||
"label": I18n.tr("Lock"),
|
||||
"key": "L"
|
||||
}
|
||||
case "suspend":
|
||||
return {
|
||||
"icon": "bedtime",
|
||||
"label": I18n.tr("Suspend"),
|
||||
"key": "S"
|
||||
}
|
||||
case "hibernate":
|
||||
return {
|
||||
"icon": "ac_unit",
|
||||
"label": I18n.tr("Hibernate"),
|
||||
"key": "H"
|
||||
}
|
||||
case "restart":
|
||||
return {
|
||||
"icon": "refresh",
|
||||
"label": I18n.tr("Restart DMS"),
|
||||
"key": "D"
|
||||
}
|
||||
default:
|
||||
return {
|
||||
"icon": "help",
|
||||
"label": action,
|
||||
"key": "?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectOption(action) {
|
||||
if (action === "lock") {
|
||||
close()
|
||||
lockRequested()
|
||||
return
|
||||
}
|
||||
if (action === "restart") {
|
||||
close()
|
||||
Quickshell.execDetached(["dms", "restart"])
|
||||
return
|
||||
}
|
||||
close()
|
||||
const actions = {
|
||||
"logout": {
|
||||
"title": I18n.tr("Log Out"),
|
||||
"message": I18n.tr("Are you sure you want to log out?")
|
||||
},
|
||||
"suspend": {
|
||||
"title": I18n.tr("Suspend"),
|
||||
"message": I18n.tr("Are you sure you want to suspend the system?")
|
||||
},
|
||||
"hibernate": {
|
||||
"title": I18n.tr("Hibernate"),
|
||||
"message": I18n.tr("Are you sure you want to hibernate the system?")
|
||||
},
|
||||
"reboot": {
|
||||
"title": I18n.tr("Reboot"),
|
||||
"message": I18n.tr("Are you sure you want to reboot the system?")
|
||||
},
|
||||
"poweroff": {
|
||||
"title": I18n.tr("Power Off"),
|
||||
"message": I18n.tr("Are you sure you want to power off the system?")
|
||||
}
|
||||
}
|
||||
const selected = actions[action]
|
||||
if (selected) {
|
||||
root.powerActionRequested(action, selected.title, selected.message)
|
||||
}
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
width: Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2)
|
||||
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
|
||||
enableShadow: true
|
||||
screen: parentScreen
|
||||
positioning: parentBounds.width > 0 ? "custom" : "center"
|
||||
customPosition: {
|
||||
if (parentBounds.width > 0) {
|
||||
const centerX = parentBounds.x + (parentBounds.width - width) / 2
|
||||
const centerY = parentBounds.y + (parentBounds.height - height) / 2
|
||||
return Qt.point(centerX, centerY)
|
||||
}
|
||||
return Qt.point(0, 0)
|
||||
}
|
||||
onBackgroundClicked: () => close()
|
||||
onOpened: () => {
|
||||
updateVisibleActions()
|
||||
const defaultIndex = getDefaultActionIndex()
|
||||
selectedRow = Math.floor(defaultIndex / gridColumns)
|
||||
selectedCol = defaultIndex % gridColumns
|
||||
Qt.callLater(() => modalFocusScope.forceActiveFocus())
|
||||
}
|
||||
Component.onCompleted: updateVisibleActions()
|
||||
modalFocusScope.Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Left:
|
||||
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Right:
|
||||
selectedCol = (selectedCol + 1) % gridColumns
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Up:
|
||||
case Qt.Key_Backtab:
|
||||
selectedRow = (selectedRow - 1 + gridRows) % gridRows
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Down:
|
||||
case Qt.Key_Tab:
|
||||
selectedRow = (selectedRow + 1) % gridRows
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
selectOption(getActionAtIndex(selectedIndex))
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedCol = (selectedCol + 1) % gridColumns
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_P:
|
||||
if (!(event.modifiers & Qt.ControlModifier)) {
|
||||
selectOption("poweroff")
|
||||
event.accepted = true
|
||||
} else {
|
||||
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedRow = (selectedRow + 1) % gridRows
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedRow = (selectedRow - 1 + gridRows) % gridRows
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_R:
|
||||
selectOption("reboot")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_X:
|
||||
selectOption("logout")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_L:
|
||||
selectOption("lock")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_S:
|
||||
selectOption("suspend")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_H:
|
||||
selectOption("hibernate")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_D:
|
||||
selectOption("restart")
|
||||
event.accepted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
implicitHeight: buttonGrid.implicitHeight + Theme.spacingL * 2
|
||||
|
||||
Grid {
|
||||
id: buttonGrid
|
||||
anchors.centerIn: parent
|
||||
columns: root.gridColumns
|
||||
columnSpacing: Theme.spacingS
|
||||
rowSpacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.visibleActions
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
readonly property bool isSelected: root.selectedIndex === index
|
||||
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
|
||||
|
||||
width: (root.width - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
||||
height: 100
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isSelected)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
if (mouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
border.color: isSelected ? Theme.primary : "transparent"
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: parent.parent.actionData.icon
|
||||
size: Theme.iconSize + 8
|
||||
color: {
|
||||
if (parent.parent.showWarning && mouseArea.containsMouse) {
|
||||
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
|
||||
}
|
||||
return Theme.surfaceText
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: parent.parent.actionData.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (parent.parent.showWarning && mouseArea.containsMouse) {
|
||||
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
|
||||
}
|
||||
return Theme.surfaceText
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 16
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: parent.parent.parent.actionData.key
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.selectedRow = Math.floor(index / root.gridColumns)
|
||||
root.selectedCol = index % root.gridColumns
|
||||
root.selectOption(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: processListModal
|
||||
|
||||
layerNamespace: "dms:process-list-modal"
|
||||
|
||||
property int currentTab: 0
|
||||
property var tabNames: ["Processes", "Performance", "System"]
|
||||
|
||||
function show() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
return ;
|
||||
}
|
||||
open();
|
||||
UserInfoService.getUptime();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close();
|
||||
if (processContextMenu.visible) {
|
||||
processContextMenu.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
return ;
|
||||
}
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
width: 900
|
||||
height: 680
|
||||
visible: false
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
enableShadow: true
|
||||
onBackgroundClicked: () => {
|
||||
return hide();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: processesTabComponent
|
||||
|
||||
ProcessesTab {
|
||||
contextMenu: processContextMenu
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: performanceTabComponent
|
||||
|
||||
PerformanceTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemTabComponent
|
||||
|
||||
SystemTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ProcessContextMenu {
|
||||
id: processContextMenu
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
processListModal.hide();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_1) {
|
||||
currentTab = 0;
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_2) {
|
||||
currentTab = 1;
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_3) {
|
||||
currentTab = 2;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message when dgop is not available
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 400
|
||||
height: 200
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
|
||||
border.color: Theme.error
|
||||
border.width: 2
|
||||
visible: !DgopService.dgopAvailable
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
|
||||
DankIcon {
|
||||
name: "error"
|
||||
size: 48
|
||||
color: Theme.error
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Monitor Unavailable")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.error
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
visible: DgopService.dgopAvailable
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
height: 40
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Monitor")
|
||||
font.pixelSize: Theme.fontSizeLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
return processListModal.hide();
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: tabNames
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||
return tabIcons[index] || "tab";
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
currentTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Loader {
|
||||
id: processesTab
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
opacity: currentTab === 0 ? 1 : 0
|
||||
sourceComponent: processesTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: performanceTab
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 1
|
||||
visible: currentTab === 1
|
||||
opacity: currentTab === 1 ? 1 : 0
|
||||
sourceComponent: performanceTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemTab
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
opacity: currentTab === 2 ? 1 : 0
|
||||
sourceComponent: systemTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,780 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: powerTab
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingL
|
||||
clip: true
|
||||
contentHeight: mainColumn.height
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: lockScreenSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "lock"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Lock Screen")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Power Actions")
|
||||
description: I18n.tr("Show power, restart, and logout buttons on the lock screen")
|
||||
checked: SettingsData.lockScreenShowPowerActions
|
||||
onToggled: checked => SettingsData.set("lockScreenShowPowerActions", checked)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("loginctl not available - lock integration requires DMS socket connection")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.warning
|
||||
visible: !SessionService.loginctlAvailable
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Enable loginctl lock integration")
|
||||
description: I18n.tr("Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen")
|
||||
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
enabled: SessionService.loginctlAvailable
|
||||
onToggled: checked => {
|
||||
if (SessionService.loginctlAvailable) {
|
||||
SettingsData.set("loginctlLockIntegration", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Lock before suspend")
|
||||
description: I18n.tr("Automatically lock the screen when the system prepares to suspend")
|
||||
checked: SettingsData.lockBeforeSuspend
|
||||
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
onToggled: checked => SettingsData.set("lockBeforeSuspend", 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.set("enableFprint", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: timeoutSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: timeoutSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "schedule"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Idle Settings")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, parent.width - parent.children[0].width - parent.children[1].width - powerCategory.width - Theme.spacingM * 3)
|
||||
height: parent.height
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: powerCategory
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: BatteryService.batteryAvailable
|
||||
model: ["AC Power", "Battery"]
|
||||
currentIndex: 0
|
||||
selectionMode: "single"
|
||||
checkEnabled: false
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Prevent idle for media")
|
||||
description: I18n.tr("Inhibit idle timeout when audio or video is playing")
|
||||
checked: SettingsData.preventIdleForMedia
|
||||
visible: IdleService.idleMonitorAvailable
|
||||
onToggled: checked => SettingsData.set("preventIdleForMedia", checked)
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: lockDropdown
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Automatically lock after")
|
||||
options: timeoutOptions
|
||||
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout
|
||||
const index = lockDropdown.timeoutValues.indexOf(currentTimeout)
|
||||
lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout
|
||||
const index = timeoutValues.indexOf(currentTimeout)
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acLockTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batteryLockTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: monitorDropdown
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Turn off monitors after")
|
||||
options: timeoutOptions
|
||||
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout
|
||||
const index = monitorDropdown.timeoutValues.indexOf(currentTimeout)
|
||||
monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout
|
||||
const index = timeoutValues.indexOf(currentTimeout)
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acMonitorTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batteryMonitorTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: suspendDropdown
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Suspend system after")
|
||||
options: timeoutOptions
|
||||
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout
|
||||
const index = suspendDropdown.timeoutValues.indexOf(currentTimeout)
|
||||
suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout
|
||||
const index = timeoutValues.indexOf(currentTimeout)
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batterySuspendTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: SessionService.hibernateSupported
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Suspend behavior")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
leftPadding: Theme.spacingM
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: suspendBehaviorSelector
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
model: ["Suspend", "Hibernate", "Suspend then Hibernate"]
|
||||
selectionMode: "single"
|
||||
checkEnabled: false
|
||||
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const behavior = powerCategory.currentIndex === 0 ? SettingsData.acSuspendBehavior : SettingsData.batterySuspendBehavior
|
||||
suspendBehaviorSelector.currentIndex = behavior
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const behavior = powerCategory.currentIndex === 0 ? SettingsData.acSuspendBehavior : SettingsData.batterySuspendBehavior
|
||||
currentIndex = behavior
|
||||
}
|
||||
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected) {
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendBehavior", index)
|
||||
} else {
|
||||
SettingsData.set("batterySuspendBehavior", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: !IdleService.idleMonitorAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: powerMenuCustomSection.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: powerMenuCustomSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "tune"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Menu Customization")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Customize which actions appear in the power menu")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: defaultActionDropdown
|
||||
width: parent.width
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Default selected action")
|
||||
options: ["Reboot", "Log Out", "Power Off", "Lock", "Suspend", "Restart DMS", "Hibernate"]
|
||||
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"]
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentAction = SettingsData.powerMenuDefaultAction || "logout"
|
||||
const index = actionValues.indexOf(currentAction)
|
||||
currentValue = index >= 0 ? options[index] : "Log Out"
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = options.indexOf(value)
|
||||
if (index >= 0) {
|
||||
SettingsData.set("powerMenuDefaultAction", actionValues[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Reboot")
|
||||
checked: SettingsData.powerMenuActions.includes("reboot")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("reboot")) {
|
||||
actions.push("reboot")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "reboot")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Log Out")
|
||||
checked: SettingsData.powerMenuActions.includes("logout")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("logout")) {
|
||||
actions.push("logout")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "logout")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Power Off")
|
||||
checked: SettingsData.powerMenuActions.includes("poweroff")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("poweroff")) {
|
||||
actions.push("poweroff")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "poweroff")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Lock")
|
||||
checked: SettingsData.powerMenuActions.includes("lock")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("lock")) {
|
||||
actions.push("lock")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "lock")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Suspend")
|
||||
checked: SettingsData.powerMenuActions.includes("suspend")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("suspend")) {
|
||||
actions.push("suspend")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "suspend")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Restart DMS")
|
||||
description: I18n.tr("Restart the DankMaterialShell")
|
||||
checked: SettingsData.powerMenuActions.includes("restart")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("restart")) {
|
||||
actions.push("restart")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "restart")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Hibernate")
|
||||
description: I18n.tr("Only visible if hibernate is supported by your system")
|
||||
checked: SettingsData.powerMenuActions.includes("hibernate")
|
||||
visible: SessionService.hibernateSupported
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("hibernate")) {
|
||||
actions.push("hibernate")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "hibernate")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.set("powerActionConfirm", 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.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionLock) {
|
||||
text = SettingsData.customPowerActionLock
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionLock", 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.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionLogout) {
|
||||
text = SettingsData.customPowerActionLogout
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionLogout", 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.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionSuspend) {
|
||||
text = SettingsData.customPowerActionSuspend
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionSuspend", 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.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionHibernate) {
|
||||
text = SettingsData.customPowerActionHibernate
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionHibernate", 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.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionReboot) {
|
||||
text = SettingsData.customPowerActionReboot
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionReboot", 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.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionPowerOff) {
|
||||
text = SettingsData.customPowerActionPowerOff
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionPowerOff", text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.Settings
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property int currentIndex: 0
|
||||
property var parentModal: null
|
||||
|
||||
focus: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
anchors.topMargin: 0
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: personalizationLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 0
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: Component {
|
||||
PersonalizationTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: timeWeatherLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 1
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: TimeWeatherTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: topBarLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 2
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: DankBarTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: widgetsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 3
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: WidgetTweaksTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dockLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 4
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: Component {
|
||||
DockTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: displaysLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 5
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: DisplaysTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: launcherLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 6
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: LauncherTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: themeColorsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 7
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: ThemeColorsTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: powerLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 8
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: PowerSettings {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: pluginsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 9
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: PluginsTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: aboutLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 10
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: AboutTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: settingsModal
|
||||
|
||||
layerNamespace: "dms:settings"
|
||||
|
||||
property Component settingsContent
|
||||
property alias profileBrowser: profileBrowser
|
||||
property int currentTabIndex: 0
|
||||
|
||||
signal closingModal()
|
||||
|
||||
function show() {
|
||||
open();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
objectName: "settingsModal"
|
||||
width: Math.min(800, screenWidth * 0.9)
|
||||
height: Math.min(800, screenHeight * 0.85)
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
visible: false
|
||||
onBackgroundClicked: () => {
|
||||
return hide();
|
||||
}
|
||||
content: settingsContent
|
||||
onOpened: () => {
|
||||
Qt.callLater(() => {
|
||||
modalFocusScope.forceActiveFocus()
|
||||
if (contentLoader.item) {
|
||||
contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
modalFocusScope.forceActiveFocus()
|
||||
if (contentLoader.item) {
|
||||
contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
modalFocusScope.Keys.onPressed: event => {
|
||||
const tabCount = 11
|
||||
if (event.key === Qt.Key_Down) {
|
||||
currentTabIndex = (currentTabIndex + 1) % tabCount
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Tab && !event.modifiers) {
|
||||
currentTabIndex = (currentTabIndex + 1) % tabCount
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
|
||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
settingsModal.show();
|
||||
return "SETTINGS_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
settingsModal.hide();
|
||||
return "SETTINGS_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
settingsModal.toggle();
|
||||
return "SETTINGS_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "settings"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function browse(type: string) {
|
||||
if (type === "wallpaper") {
|
||||
wallpaperBrowser.allowStacking = false;
|
||||
wallpaperBrowser.open();
|
||||
} else if (type === "profile") {
|
||||
profileBrowser.allowStacking = false;
|
||||
profileBrowser.open();
|
||||
}
|
||||
}
|
||||
|
||||
target: "file"
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: profileBrowser
|
||||
|
||||
allowStacking: true
|
||||
parentModal: settingsModal
|
||||
browserTitle: "Select Profile Image"
|
||||
browserIcon: "person"
|
||||
browserType: "profile"
|
||||
showHiddenFiles: true
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||
onFileSelected: (path) => {
|
||||
PortalService.setProfileImage(path);
|
||||
close();
|
||||
}
|
||||
onDialogClosed: () => {
|
||||
allowStacking = true;
|
||||
if (settingsModal.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
settingsModal.modalFocusScope.forceActiveFocus()
|
||||
if (settingsModal.contentLoader.item) {
|
||||
settingsModal.contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: wallpaperBrowser
|
||||
|
||||
allowStacking: true
|
||||
parentModal: settingsModal
|
||||
browserTitle: "Select Wallpaper"
|
||||
browserIcon: "wallpaper"
|
||||
browserType: "wallpaper"
|
||||
showHiddenFiles: true
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||
onFileSelected: (path) => {
|
||||
SessionData.setWallpaper(path);
|
||||
close();
|
||||
}
|
||||
onDialogClosed: () => {
|
||||
allowStacking = true;
|
||||
if (settingsModal.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
settingsModal.modalFocusScope.forceActiveFocus()
|
||||
if (settingsModal.contentLoader.item) {
|
||||
settingsModal.contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settingsContent: Component {
|
||||
Item {
|
||||
id: rootScope
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
settingsModal.hide()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 35
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Settings")
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
return settingsModal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: parent.height - 35
|
||||
spacing: 0
|
||||
|
||||
SettingsSidebar {
|
||||
id: sidebar
|
||||
|
||||
parentModal: settingsModal
|
||||
currentIndex: settingsModal.currentTabIndex
|
||||
onCurrentIndexChanged: {
|
||||
settingsModal.currentTabIndex = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
SettingsContent {
|
||||
id: content
|
||||
|
||||
width: parent.width - sidebar.width
|
||||
height: parent.height
|
||||
parentModal: settingsModal
|
||||
currentIndex: settingsModal.currentTabIndex
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Settings
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: sidebarContainer
|
||||
|
||||
property int currentIndex: 0
|
||||
property var parentModal: null
|
||||
readonly property var sidebarItems: [{
|
||||
"text": I18n.tr("Personalization"),
|
||||
"icon": "person"
|
||||
}, {
|
||||
"text": I18n.tr("Time & Weather"),
|
||||
"icon": "schedule"
|
||||
}, {
|
||||
"text": I18n.tr("Dank Bar"),
|
||||
"icon": "toolbar"
|
||||
}, {
|
||||
"text": I18n.tr("Widgets"),
|
||||
"icon": "widgets"
|
||||
}, {
|
||||
"text": I18n.tr("Dock"),
|
||||
"icon": "dock_to_bottom"
|
||||
}, {
|
||||
"text": I18n.tr("Displays"),
|
||||
"icon": "monitor"
|
||||
}, {
|
||||
"text": I18n.tr("Launcher"),
|
||||
"icon": "apps"
|
||||
}, {
|
||||
"text": I18n.tr("Theme & Colors"),
|
||||
"icon": "palette"
|
||||
}, {
|
||||
"text": I18n.tr("Power & Security"),
|
||||
"icon": "power"
|
||||
}, {
|
||||
"text": I18n.tr("Plugins"),
|
||||
"icon": "extension"
|
||||
}, {
|
||||
"text": I18n.tr("About"),
|
||||
"icon": "info"
|
||||
}]
|
||||
|
||||
function navigateNext() {
|
||||
currentIndex = (currentIndex + 1) % sidebarItems.length
|
||||
}
|
||||
|
||||
function navigatePrevious() {
|
||||
currentIndex = (currentIndex - 1 + sidebarItems.length) % sidebarItems.length
|
||||
}
|
||||
|
||||
width: 270
|
||||
height: parent.height
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: sidebarColumn.implicitHeight
|
||||
|
||||
Column {
|
||||
id: sidebarColumn
|
||||
|
||||
width: parent.width
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
anchors.topMargin: Theme.spacingM + 2
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
ProfileSection {
|
||||
parentModal: sidebarContainer.parentModal
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.spacingL
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sidebarRepeater
|
||||
|
||||
model: sidebarContainer.sidebarItems
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
property bool isActive: sidebarContainer.currentIndex === index
|
||||
|
||||
width: sidebarColumn.width - Theme.spacingS * 2
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || ""
|
||||
size: Theme.iconSize - 2
|
||||
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
|
||||
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
sidebarContainer.currentIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: spotlightKeyHandler
|
||||
|
||||
property alias appLauncher: appLauncher
|
||||
property alias searchField: searchField
|
||||
property alias fileSearchController: fileSearchController
|
||||
property var parentModal: null
|
||||
property string searchMode: "apps"
|
||||
|
||||
function resetScroll() {
|
||||
if (searchMode === "apps") {
|
||||
resultsView.resetScroll()
|
||||
} else {
|
||||
fileSearchResults.resetScroll()
|
||||
}
|
||||
}
|
||||
|
||||
function updateSearchMode() {
|
||||
if (searchField.text.startsWith("/")) {
|
||||
if (searchMode !== "files") {
|
||||
searchMode = "files"
|
||||
}
|
||||
const query = searchField.text.substring(1)
|
||||
fileSearchController.searchQuery = query
|
||||
} else {
|
||||
if (searchMode !== "apps") {
|
||||
searchMode = "apps"
|
||||
fileSearchController.reset()
|
||||
appLauncher.searchQuery = searchField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSearchModeChanged: {
|
||||
if (searchMode === "files") {
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
} else {
|
||||
fileSearchController.keyboardNavigationActive = false
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
clip: false
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectNext()
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectPrevious()
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectNext()
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectPrevious()
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Tab) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
} else {
|
||||
appLauncher.selectNext()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Backtab) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
} else {
|
||||
appLauncher.selectPrevious()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
} else {
|
||||
appLauncher.selectNext()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
} else {
|
||||
appLauncher.selectPrevious()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.launchSelected()
|
||||
} else if (searchMode === "files") {
|
||||
fileSearchController.openSelected()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
AppLauncher {
|
||||
id: appLauncher
|
||||
|
||||
viewMode: SettingsData.spotlightModalViewMode
|
||||
gridColumns: 4
|
||||
onAppLaunched: () => {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
}
|
||||
onViewModeSelected: mode => {
|
||||
SettingsData.set("spotlightModalViewMode", mode)
|
||||
}
|
||||
}
|
||||
|
||||
FileSearchController {
|
||||
id: fileSearchController
|
||||
|
||||
onFileOpened: () => {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
clip: false
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
leftPadding: Theme.spacingS
|
||||
topPadding: Theme.spacingS
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width - 80 - Theme.spacingL
|
||||
height: 56
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: searchMode === "files" ? "folder" : "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: parentModal ? parentModal.spotlightOpen : true
|
||||
placeholderText: ""
|
||||
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
|
||||
ignoreTabKeys: true
|
||||
keyForwardTargets: [spotlightKeyHandler]
|
||||
onTextChanged: {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.searchQuery = text
|
||||
}
|
||||
}
|
||||
onTextEdited: {
|
||||
updateSearchMode()
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
|
||||
event.accepted = true
|
||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
||||
appLauncher.launchSelected()
|
||||
else if (appLauncher.model.count > 0)
|
||||
appLauncher.launchApp(appLauncher.model.get(0))
|
||||
} else if (searchMode === "files") {
|
||||
if (fileSearchController.model.count > 0)
|
||||
fileSearchController.openSelected()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key
|
||||
=== Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: searchMode === "apps" && appLauncher.model.count > 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "view_list"
|
||||
size: 18
|
||||
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "grid_view"
|
||||
size: 18
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("grid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: searchMode === "files"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: filenameFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "title"
|
||||
size: 18
|
||||
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: filenameFilterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
fileSearchController.searchField = "filename"
|
||||
}
|
||||
onEntered: {
|
||||
filenameTooltipLoader.active = true
|
||||
Qt.callLater(() => {
|
||||
if (filenameTooltipLoader.item) {
|
||||
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
|
||||
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
onExited: {
|
||||
if (filenameTooltipLoader.item)
|
||||
filenameTooltipLoader.item.hide()
|
||||
|
||||
filenameTooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "description"
|
||||
size: 18
|
||||
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: contentFilterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
fileSearchController.searchField = "body"
|
||||
}
|
||||
onEntered: {
|
||||
contentTooltipLoader.active = true
|
||||
Qt.callLater(() => {
|
||||
if (contentTooltipLoader.item) {
|
||||
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
|
||||
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
onExited: {
|
||||
if (contentTooltipLoader.item)
|
||||
contentTooltipLoader.item.hide()
|
||||
|
||||
contentTooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
|
||||
SpotlightResults {
|
||||
id: resultsView
|
||||
anchors.fill: parent
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
contextMenu: contextMenu
|
||||
visible: searchMode === "apps"
|
||||
}
|
||||
|
||||
FileSearchResults {
|
||||
id: fileSearchResults
|
||||
anchors.fill: parent
|
||||
fileSearchController: spotlightKeyHandler.fileSearchController
|
||||
visible: searchMode === "files"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
parentHandler: spotlightKeyHandler
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: contextMenu.visible
|
||||
z: 999
|
||||
onClicked: () => {
|
||||
contextMenu.hide()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
x: contextMenu.x
|
||||
y: contextMenu.y
|
||||
width: contextMenu.width
|
||||
height: contextMenu.height
|
||||
onClicked: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: filenameTooltipLoader
|
||||
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentTooltipLoader
|
||||
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: contextMenu
|
||||
|
||||
property var currentApp: null
|
||||
property var appLauncher: null
|
||||
property var parentHandler: null
|
||||
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
|
||||
|
||||
function show(x, y, app) {
|
||||
currentApp = app
|
||||
contextMenu.x = x + 4
|
||||
contextMenu.y = y + 4
|
||||
contextMenu.open()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
contextMenu.close()
|
||||
}
|
||||
|
||||
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
modal: false
|
||||
dim: false
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: pinRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (!desktopEntry)
|
||||
return "push_pin"
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!desktopEntry)
|
||||
return I18n.tr("Pin to Dock")
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (!desktopEntry)
|
||||
return
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
if (SessionData.isPinnedApp(appId))
|
||||
SessionData.removePinnedApp(appId)
|
||||
else
|
||||
SessionData.addPinnedApp(appId)
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: desktopEntry && desktopEntry.actions ? desktopEntry.actions : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: actionRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.iconSize - 2
|
||||
height: Theme.iconSize - 2
|
||||
visible: modelData.icon && modelData.icon !== ""
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && desktopEntry) {
|
||||
SessionService.launchDesktopAction(desktopEntry, modelData)
|
||||
if (appLauncher && contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: desktopEntry && desktopEntry.actions && desktopEntry.actions.length > 0
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: launchRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "launch"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: launchMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (contextMenu.currentApp && appLauncher)
|
||||
appLauncher.launchApp(contextMenu.currentApp)
|
||||
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.hasPrimeRun
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.hasPrimeRun
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: primeRunRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch on dGPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: primeRunMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (desktopEntry) {
|
||||
SessionService.launchDesktopEntry(desktopEntry, true)
|
||||
if (appLauncher && contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: spotlightModal
|
||||
|
||||
layerNamespace: "dms:spotlight"
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property alias spotlightContent: spotlightContentInstance
|
||||
|
||||
function show() {
|
||||
spotlightOpen = true
|
||||
open()
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = query
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query
|
||||
}
|
||||
}
|
||||
|
||||
spotlightOpen = true
|
||||
open()
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
spotlightOpen = false
|
||||
close()
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = ""
|
||||
spotlightContent.appLauncher.selectedIndex = 0
|
||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
|
||||
}
|
||||
if (spotlightContent.fileSearchController) {
|
||||
spotlightContent.fileSearchController.reset()
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll()
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (spotlightOpen) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
shouldBeVisible: spotlightOpen
|
||||
width: 500
|
||||
height: 600
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
enableShadow: true
|
||||
keepContentLoaded: true
|
||||
onVisibleChanged: () => {
|
||||
if (visible && !spotlightOpen) {
|
||||
show()
|
||||
}
|
||||
if (visible && spotlightContent) {
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
return hide()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
|
||||
spotlightOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
target: ModalManager
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
spotlightModal.show()
|
||||
return "SPOTLIGHT_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
spotlightModal.hide()
|
||||
return "SPOTLIGHT_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
spotlightModal.toggle()
|
||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
function openQuery(query: string): string {
|
||||
spotlightModal.showWithQuery(query)
|
||||
return "SPOTLIGHT_OPEN_QUERY_SUCCESS"
|
||||
}
|
||||
|
||||
function toggleQuery(query: string): string {
|
||||
if (spotlightModal.spotlightOpen) {
|
||||
spotlightModal.hide()
|
||||
} else {
|
||||
spotlightModal.showWithQuery(query)
|
||||
}
|
||||
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS"
|
||||
}
|
||||
|
||||
target: "spotlight"
|
||||
}
|
||||
|
||||
SpotlightContent {
|
||||
id: spotlightContentInstance
|
||||
|
||||
parentModal: spotlightModal
|
||||
}
|
||||
|
||||
directContent: spotlightContentInstance
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: resultsContainer
|
||||
|
||||
property var appLauncher: null
|
||||
property var contextMenu: null
|
||||
|
||||
function resetScroll() {
|
||||
resultsList.contentY = 0
|
||||
resultsGrid.contentY = 0
|
||||
}
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
DankListView {
|
||||
id: resultsList
|
||||
|
||||
property int itemHeight: 60
|
||||
property int iconSize: 40
|
||||
property bool showDescription: true
|
||||
property int itemSpacing: Theme.spacingS
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
const itemY = index * (itemHeight + itemSpacing)
|
||||
const itemBottom = itemY + itemHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appLauncher && appLauncher.viewMode === "list"
|
||||
model: appLauncher ? appLauncher.model : null
|
||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||
clip: true
|
||||
spacing: itemSpacing
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: (index, modelData) => {
|
||||
if (appLauncher)
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||
if (contextMenu)
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: () => {
|
||||
if (appLauncher)
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
delegate: AppLauncherListDelegate {
|
||||
listView: resultsList
|
||||
itemHeight: resultsList.itemHeight
|
||||
iconSize: resultsList.iconSize
|
||||
showDescription: resultsList.showDescription
|
||||
hoverUpdatesSelection: resultsList.hoverUpdatesSelection
|
||||
keyboardNavigationActive: resultsList.keyboardNavigationActive
|
||||
isCurrentItem: ListView.isCurrentItem
|
||||
iconMaterialSizeAdjustment: 0
|
||||
iconUnicodeScale: 0.8
|
||||
onItemClicked: (idx, modelData) => resultsList.itemClicked(idx, modelData)
|
||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
||||
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
|
||||
resultsList.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
|
||||
}
|
||||
onKeyboardNavigationReset: resultsList.keyboardNavigationReset
|
||||
}
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: resultsGrid
|
||||
|
||||
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||
property int columns: 4
|
||||
property bool adaptiveColumns: false
|
||||
property int minCellWidth: 120
|
||||
property int maxCellWidth: 160
|
||||
property int cellPadding: 8
|
||||
property real iconSizeRatio: 0.55
|
||||
property int maxIconSize: 48
|
||||
property int minIconSize: 32
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||
property int baseCellHeight: baseCellWidth + 20
|
||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
const itemY = Math.floor(index / actualColumns) * cellHeight
|
||||
const itemBottom = itemY + cellHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appLauncher && appLauncher.viewMode === "grid"
|
||||
model: appLauncher ? appLauncher.model : null
|
||||
clip: true
|
||||
cellWidth: baseCellWidth
|
||||
cellHeight: baseCellHeight
|
||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||
rightMargin: leftMargin
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: (index, modelData) => {
|
||||
if (appLauncher)
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||
if (contextMenu)
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: () => {
|
||||
if (appLauncher)
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
delegate: AppLauncherGridDelegate {
|
||||
gridView: resultsGrid
|
||||
cellWidth: resultsGrid.cellWidth
|
||||
cellHeight: resultsGrid.cellHeight
|
||||
cellPadding: resultsGrid.cellPadding
|
||||
minIconSize: resultsGrid.minIconSize
|
||||
maxIconSize: resultsGrid.maxIconSize
|
||||
iconSizeRatio: resultsGrid.iconSizeRatio
|
||||
hoverUpdatesSelection: resultsGrid.hoverUpdatesSelection
|
||||
keyboardNavigationActive: resultsGrid.keyboardNavigationActive
|
||||
currentIndex: resultsGrid.currentIndex
|
||||
onItemClicked: (idx, modelData) => resultsGrid.itemClicked(idx, modelData)
|
||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
||||
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
|
||||
resultsGrid.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
|
||||
}
|
||||
onKeyboardNavigationReset: resultsGrid.keyboardNavigationReset
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,594 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:wifi-password"
|
||||
|
||||
property string wifiPasswordSSID: ""
|
||||
property string wifiPasswordInput: ""
|
||||
property string wifiUsernameInput: ""
|
||||
property bool requiresEnterprise: false
|
||||
|
||||
property string wifiAnonymousIdentityInput: ""
|
||||
property string wifiDomainInput: ""
|
||||
|
||||
property bool isPromptMode: false
|
||||
property string promptToken: ""
|
||||
property string promptReason: ""
|
||||
property var promptFields: []
|
||||
property string promptSetting: ""
|
||||
|
||||
property bool isVpnPrompt: false
|
||||
property string connectionName: ""
|
||||
property string vpnServiceType: ""
|
||||
property string connectionType: ""
|
||||
|
||||
function show(ssid) {
|
||||
wifiPasswordSSID = ssid
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
isPromptMode = false
|
||||
promptToken = ""
|
||||
promptReason = ""
|
||||
promptFields = []
|
||||
promptSetting = ""
|
||||
isVpnPrompt = false
|
||||
connectionName = ""
|
||||
vpnServiceType = ""
|
||||
connectionType = ""
|
||||
|
||||
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
|
||||
requiresEnterprise = network?.enterprise || false
|
||||
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item) {
|
||||
if (requiresEnterprise && contentLoader.item.usernameInput) {
|
||||
contentLoader.item.usernameInput.forceActiveFocus()
|
||||
} else if (contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
|
||||
isPromptMode = true
|
||||
promptToken = token
|
||||
promptReason = reason
|
||||
promptFields = fields || []
|
||||
promptSetting = setting || "802-11-wireless-security"
|
||||
connectionType = connType || "802-11-wireless"
|
||||
connectionName = connName || ssid || ""
|
||||
vpnServiceType = vpnService || ""
|
||||
|
||||
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard")
|
||||
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid
|
||||
|
||||
requiresEnterprise = setting === "802-1x"
|
||||
|
||||
if (reason === "wrong-password") {
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
} else {
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item) {
|
||||
if (reason === "wrong-password" && contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.text = ""
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
} else if (requiresEnterprise && contentLoader.item.usernameInput) {
|
||||
contentLoader.item.usernameInput.forceActiveFocus()
|
||||
} else if (contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
width: 420
|
||||
height: requiresEnterprise ? 430 : 230
|
||||
onShouldBeVisibleChanged: () => {
|
||||
if (!shouldBeVisible) {
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
onOpened: {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item) {
|
||||
if (requiresEnterprise && contentLoader.item.usernameInput) {
|
||||
contentLoader.item.usernameInput.forceActiveFocus()
|
||||
} else if (contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
if (isPromptMode) {
|
||||
NetworkService.cancelCredentials(promptToken)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NetworkService
|
||||
|
||||
function onPasswordDialogShouldReopenChanged() {
|
||||
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
|
||||
wifiPasswordSSID = NetworkService.connectingSSID
|
||||
wifiPasswordInput = ""
|
||||
open()
|
||||
NetworkService.passwordDialogShouldReopen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: wifiContent
|
||||
|
||||
property alias usernameInput: usernameInput
|
||||
property alias passwordInput: passwordInput
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onEscapePressed: event => {
|
||||
if (isPromptMode) {
|
||||
NetworkService.cancelCredentials(promptToken)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (isVpnPrompt) {
|
||||
return I18n.tr("Connect to VPN")
|
||||
}
|
||||
return I18n.tr("Connect to Wi-Fi")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (isVpnPrompt) {
|
||||
return I18n.tr("Enter password for ") + wifiPasswordSSID
|
||||
}
|
||||
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ")
|
||||
return prefix + wifiPasswordSSID
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: isPromptMode && promptReason === "wrong-password"
|
||||
text: I18n.tr("Incorrect password")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
if (isPromptMode) {
|
||||
NetworkService.cancelCredentials(promptToken)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: usernameInput.activeFocus ? 2 : 1
|
||||
visible: requiresEnterprise && !isVpnPrompt
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
usernameInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: usernameInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiUsernameInput
|
||||
placeholderText: I18n.tr("Username")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiUsernameInput = text
|
||||
}
|
||||
onAccepted: () => {
|
||||
if (passwordInput) {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: passwordInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiPasswordInput
|
||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
|
||||
backgroundColor: "transparent"
|
||||
focus: !requiresEnterprise
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiPasswordInput = text
|
||||
}
|
||||
onAccepted: () => {
|
||||
if (isPromptMode) {
|
||||
const secrets = {}
|
||||
if (isVpnPrompt) {
|
||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
||||
} else if (promptSetting === "802-11-wireless-security") {
|
||||
secrets["psk"] = passwordInput.text
|
||||
} else if (promptSetting === "802-1x") {
|
||||
if (usernameInput.text) secrets["identity"] = usernameInput.text
|
||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
||||
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
|
||||
}
|
||||
NetworkService.submitCredentials(promptToken, secrets, true)
|
||||
} else {
|
||||
const username = requiresEnterprise ? usernameInput.text : ""
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text,
|
||||
username,
|
||||
wifiAnonymousIdentityInput,
|
||||
wifiDomainInput
|
||||
)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
passwordInput.text = ""
|
||||
if (requiresEnterprise) usernameInput.text = ""
|
||||
}
|
||||
Component.onCompleted: () => {
|
||||
if (root.shouldBeVisible && !requiresEnterprise)
|
||||
focusDelayTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: focusDelayTimer
|
||||
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: () => {
|
||||
if (root.shouldBeVisible) {
|
||||
if (requiresEnterprise && usernameInput) {
|
||||
usernameInput.forceActiveFocus()
|
||||
} else {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible)
|
||||
focusDelayTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: requiresEnterprise && !isVpnPrompt
|
||||
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 && !isVpnPrompt
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: domainMatchInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
domainMatchInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: domainMatchInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiDomainInput
|
||||
placeholderText: I18n.tr("Domain (optional)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiDomainInput = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: showPasswordCheckbox
|
||||
|
||||
property bool checked: false
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: checked ? Theme.primary : "transparent"
|
||||
border.color: checked ? Theme.primary : Theme.outlineButton
|
||||
border.width: 2
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: 12
|
||||
color: Theme.background
|
||||
visible: parent.checked
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show password")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: cancelText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (isPromptMode) {
|
||||
NetworkService.cancelCredentials(promptToken)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: {
|
||||
if (isVpnPrompt) {
|
||||
return passwordInput.text.length > 0
|
||||
}
|
||||
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0
|
||||
}
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: connectText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Connect")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: () => {
|
||||
if (isPromptMode) {
|
||||
const secrets = {}
|
||||
if (isVpnPrompt) {
|
||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
||||
} else if (promptSetting === "802-11-wireless-security") {
|
||||
secrets["psk"] = passwordInput.text
|
||||
} else if (promptSetting === "802-1x") {
|
||||
if (usernameInput.text) secrets["identity"] = usernameInput.text
|
||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
||||
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
|
||||
}
|
||||
NetworkService.submitCredentials(promptToken, secrets, true)
|
||||
} else {
|
||||
const username = requiresEnterprise ? usernameInput.text : ""
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text,
|
||||
username,
|
||||
wifiAnonymousIdentityInput,
|
||||
wifiDomainInput
|
||||
)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
passwordInput.text = ""
|
||||
if (requiresEnterprise) usernameInput.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
|
||||
Ref {
|
||||
service: DMSNetworkService
|
||||
}
|
||||
|
||||
|
||||
ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
|
||||
ccWidgetPrimaryText: "VPN"
|
||||
ccWidgetSecondaryText: {
|
||||
if (!DMSNetworkService.connected)
|
||||
return "Disconnected"
|
||||
const names = DMSNetworkService.activeNames || []
|
||||
if (names.length <= 1)
|
||||
return names[0] || "Connected"
|
||||
return names[0] + " +" + (names.length - 1)
|
||||
}
|
||||
ccWidgetIsActive: DMSNetworkService.connected
|
||||
|
||||
onCcWidgetToggled: {
|
||||
DMSNetworkService.toggleVpn()
|
||||
}
|
||||
|
||||
ccDetailContent: Component {
|
||||
Rectangle {
|
||||
id: detailRoot
|
||||
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
id: detailColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
RowLayout {
|
||||
spacing: Theme.spacingS
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DMSNetworkService.connected)
|
||||
return "Active: None"
|
||||
const names = DMSNetworkService.activeNames || []
|
||||
if (names.length <= 1)
|
||||
return "Active: " + (names[0] || "VPN")
|
||||
return "Active: " + names[0] + " +" + (names.length - 1)
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: parent.width - 120
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 28
|
||||
radius: 14
|
||||
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
|
||||
visible: DMSNetworkService.connected
|
||||
width: 110
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "link_off"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disconnect")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: discAllArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onClicked: DMSNetworkService.disconnectAllActive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
width: parent.width
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
width: parent.width
|
||||
height: 160
|
||||
contentHeight: listCol.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: listCol
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: DMSNetworkService.profiles.length === 0 ? 120 : 0
|
||||
visible: height > 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "playlist_remove"
|
||||
size: 36
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No VPN profiles found")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Add a VPN in NetworkManager")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: DMSNetworkService.profiles
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
width: parent ? parent.width : 300
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
|
||||
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
|
||||
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
|
||||
size: Theme.iconSize - 4
|
||||
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (modelData.type === "wireguard")
|
||||
return "WireGuard"
|
||||
const svc = modelData.serviceType || ""
|
||||
if (svc.indexOf("openvpn") !== -1)
|
||||
return "OpenVPN"
|
||||
if (svc.indexOf("wireguard") !== -1)
|
||||
return "WireGuard (plugin)"
|
||||
if (svc.indexOf("openconnect") !== -1)
|
||||
return "OpenConnect"
|
||||
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1)
|
||||
return "Fortinet"
|
||||
if (svc.indexOf("strongswan") !== -1)
|
||||
return "IPsec (strongSwan)"
|
||||
if (svc.indexOf("libreswan") !== -1)
|
||||
return "IPsec (Libreswan)"
|
||||
if (svc.indexOf("l2tp") !== -1)
|
||||
return "L2TP/IPsec"
|
||||
if (svc.indexOf("pptp") !== -1)
|
||||
return "PPTP"
|
||||
if (svc.indexOf("vpnc") !== -1)
|
||||
return "Cisco (vpnc)"
|
||||
if (svc.indexOf("sstp") !== -1)
|
||||
return "SSTP"
|
||||
if (svc)
|
||||
return svc.split('.').pop()
|
||||
return "VPN"
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onClicked: DMSNetworkService.toggle(modelData.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
property bool hasVolumeSliderInCC: {
|
||||
const widgets = SettingsData.controlCenterWidgets || []
|
||||
return widgets.some(widget => widget.id === "volumeSlider")
|
||||
}
|
||||
|
||||
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
height: 40
|
||||
|
||||
StyledText {
|
||||
id: headerText
|
||||
text: I18n.tr("Audio Devices")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: volumeSlider
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: headerRow.bottom
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
height: 35
|
||||
spacing: 0
|
||||
visible: !hasVolumeSliderInCC
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize + Theme.spacingS * 2
|
||||
height: Theme.iconSize + Theme.spacingS * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: iconArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (!AudioService.sink || !AudioService.sink.audio) return "volume_off"
|
||||
let muted = AudioService.sink.audio.muted
|
||||
let volume = AudioService.sink.audio.volume
|
||||
if (muted || volume === 0.0) return "volume_off"
|
||||
if (volume <= 0.33) return "volume_down"
|
||||
if (volume <= 0.66) return "volume_up"
|
||||
return "volume_up"
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||
enabled: AudioService.sink && AudioService.sink.audio
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0
|
||||
showValue: true
|
||||
unit: "%"
|
||||
valueOverride: actualVolumePercent
|
||||
thumbOutlineColor: Theme.surfaceVariant
|
||||
|
||||
onSliderValueChanged: function(newValue) {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.volume = newValue / 100
|
||||
if (newValue > 0 && AudioService.sink.audio.muted) {
|
||||
AudioService.sink.audio.muted = false
|
||||
}
|
||||
AudioService.volumeChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: audioContent
|
||||
anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
|
||||
contentHeight: audioColumn.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: audioColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Pipewire.nodes.values.filter(node => {
|
||||
return node.audio && node.isSink && !node.isStream
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset"
|
||||
else if (modelData.name.includes("hdmi"))
|
||||
return "tv"
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset"
|
||||
else
|
||||
return "speaker"
|
||||
}
|
||||
size: Theme.iconSize - 4
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData === AudioService.sink ? "Active" : "Available"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
Pipewire.preferredDefaultAudioSink = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
readonly property string powerOptionsText: I18n.tr("Power Options")
|
||||
readonly property string logOutText: I18n.tr("Log Out")
|
||||
readonly property string suspendText: I18n.tr("Suspend")
|
||||
readonly property string rebootText: I18n.tr("Reboot")
|
||||
readonly property string powerOffText: I18n.tr("Power Off")
|
||||
|
||||
property bool powerMenuVisible: false
|
||||
signal powerActionRequested(string action, string title, string message)
|
||||
|
||||
visible: powerMenuVisible
|
||||
implicitWidth: 400
|
||||
implicitHeight: 320
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(320, parent.width - Theme.spacingL * 2)
|
||||
height: 320 // Fixed height to prevent cropping
|
||||
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
||||
y: Theme.barHeight + Theme.spacingXS
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
opacity: powerMenuVisible ? 1 : 0
|
||||
scale: powerMenuVisible ? 1 : 0.85
|
||||
|
||||
MouseArea {
|
||||
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: root.powerOptionsText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 150
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.08) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "logout"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.logOutText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
root.powerActionRequested(
|
||||
"logout", "Log Out",
|
||||
"Are you sure you want to log out?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.08) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.suspendText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
root.powerActionRequested(
|
||||
"suspend", "Suspend",
|
||||
"Are you sure you want to suspend the system?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
|
||||
Theme.warning.g,
|
||||
Theme.warning.b,
|
||||
0.08) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "restart_alt"
|
||||
size: Theme.iconSize
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.rebootText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
root.powerActionRequested(
|
||||
"reboot", "Reboot",
|
||||
"Are you sure you want to reboot the system?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
|
||||
Theme.error.g,
|
||||
Theme.error.b,
|
||||
0.08) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "power_settings_new"
|
||||
size: Theme.iconSize
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.powerOffText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerOffArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
root.powerActionRequested(
|
||||
"poweroff", "Power Off",
|
||||
"Are you sure you want to power off the system?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
function setTriggerPosition(root, x, y, width, section, screen) {
|
||||
root.triggerX = x
|
||||
root.triggerY = y
|
||||
root.triggerWidth = width
|
||||
root.triggerSection = section
|
||||
root.triggerScreen = screen
|
||||
}
|
||||
|
||||
function openWithSection(root, section) {
|
||||
if (root.shouldBeVisible) {
|
||||
root.close()
|
||||
} else {
|
||||
root.expandedSection = section
|
||||
root.open()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSection(root, section) {
|
||||
if (root.expandedSection === section) {
|
||||
root.expandedSection = ""
|
||||
root.expandedWidgetIndex = -1
|
||||
} else {
|
||||
root.expandedSection = section
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var barWindow
|
||||
required property var axis
|
||||
required property var appDrawerLoader
|
||||
required property var dankDashPopoutLoader
|
||||
required property var processListPopoutLoader
|
||||
required property var notificationCenterLoader
|
||||
required property var batteryPopoutLoader
|
||||
required property var vpnPopoutLoader
|
||||
required property var controlCenterLoader
|
||||
required property var clipboardHistoryModalPopup
|
||||
required property var systemUpdateLoader
|
||||
required property var notepadInstance
|
||||
|
||||
property alias reveal: core.reveal
|
||||
property alias autoHide: core.autoHide
|
||||
property alias backgroundTransparency: core.backgroundTransparency
|
||||
property alias hasActivePopout: core.hasActivePopout
|
||||
property alias mouseArea: topBarMouseArea
|
||||
|
||||
Item {
|
||||
id: inputMask
|
||||
|
||||
readonly property int barThickness: barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing)
|
||||
|
||||
readonly property bool showing: SettingsData.dankBarVisible && (core.reveal
|
||||
|| (CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview)
|
||||
|| !core.autoHide)
|
||||
|
||||
readonly property int maskThickness: showing ? barThickness : 1
|
||||
|
||||
x: {
|
||||
if (!axis.isVertical) {
|
||||
return 0
|
||||
} else {
|
||||
switch (SettingsData.dankBarPosition) {
|
||||
case SettingsData.Position.Left: return 0
|
||||
case SettingsData.Position.Right: return parent.width - maskThickness
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (axis.isVertical) {
|
||||
return 0
|
||||
} else {
|
||||
switch (SettingsData.dankBarPosition) {
|
||||
case SettingsData.Position.Top: return 0
|
||||
case SettingsData.Position.Bottom: return parent.height - maskThickness
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
width: axis.isVertical ? maskThickness : parent.width
|
||||
height: axis.isVertical ? parent.height : maskThickness
|
||||
}
|
||||
|
||||
Region {
|
||||
id: mask
|
||||
item: inputMask
|
||||
}
|
||||
|
||||
property alias maskRegion: mask
|
||||
|
||||
QtObject {
|
||||
id: core
|
||||
|
||||
property real backgroundTransparency: SettingsData.dankBarTransparency
|
||||
property bool autoHide: SettingsData.dankBarAutoHide
|
||||
property bool revealSticky: false
|
||||
|
||||
property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false
|
||||
|
||||
readonly property bool hasActivePopout: {
|
||||
const loaders = [{
|
||||
"loader": appDrawerLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": dankDashPopoutLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": processListPopoutLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": notificationCenterLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": batteryPopoutLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": vpnPopoutLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": controlCenterLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": clipboardHistoryModalPopup,
|
||||
"prop": "visible"
|
||||
}, {
|
||||
"loader": systemUpdateLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}]
|
||||
return notepadInstanceVisible || loaders.some(item => {
|
||||
if (item.loader) {
|
||||
return item.loader?.item?.[item.prop]
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
||||
return SettingsData.dankBarOpenOnOverview
|
||||
}
|
||||
return SettingsData.dankBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky)
|
||||
}
|
||||
|
||||
onHasActivePopoutChanged: {
|
||||
if (!hasActivePopout && autoHide && !topBarMouseArea.containsMouse) {
|
||||
revealSticky = true
|
||||
revealHold.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: revealHold
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: core.revealSticky = false
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onDankBarTransparencyChanged() {
|
||||
core.backgroundTransparency = SettingsData.dankBarTransparency
|
||||
}
|
||||
|
||||
target: SettingsData
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: topBarMouseArea
|
||||
function onContainsMouseChanged() {
|
||||
if (topBarMouseArea.containsMouse) {
|
||||
core.revealSticky = true
|
||||
revealHold.stop()
|
||||
} else {
|
||||
if (core.autoHide && !core.hasActivePopout) {
|
||||
revealHold.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: topBarMouseArea
|
||||
y: !barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? parent.height - height : 0) : 0
|
||||
x: barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.width - width : 0) : 0
|
||||
height: !barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
|
||||
width: barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
|
||||
anchors {
|
||||
left: !barWindow.isVertical ? parent.left : (SettingsData.dankBarPosition === SettingsData.Position.Left ? parent.left : undefined)
|
||||
right: !barWindow.isVertical ? parent.right : (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.right : undefined)
|
||||
top: barWindow.isVertical ? parent.top : undefined
|
||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||
}
|
||||
hoverEnabled: SettingsData.dankBarAutoHide && !core.reveal
|
||||
acceptedButtons: Qt.NoButton
|
||||
enabled: SettingsData.dankBarAutoHide && !core.reveal
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var barWindow
|
||||
required property var axis
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
|
||||
anchors.rightMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
|
||||
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
|
||||
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
|
||||
|
||||
readonly property real dpr: CompositorService.getScreenScale(barWindow.screen)
|
||||
|
||||
function requestRepaint() {
|
||||
debounceTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: debounceTimer
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
barShape.requestPaint()
|
||||
barTint.requestPaint()
|
||||
barBorder.requestPaint()
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: barShape
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: barWindow
|
||||
function on_BgColorChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceContainerChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
||||
const R = wing
|
||||
const RT = rt
|
||||
const H = H_raw - (R > 0 ? R : 0)
|
||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
|
||||
function drawTopPath() {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(RT, 0)
|
||||
ctx.lineTo(W - RT, 0)
|
||||
ctx.arcTo(W, 0, W, RT, RT)
|
||||
ctx.lineTo(W, H)
|
||||
|
||||
if (R > 0) {
|
||||
ctx.lineTo(W, H + R)
|
||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
||||
ctx.lineTo(R, H)
|
||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
||||
ctx.lineTo(0, H + R)
|
||||
} else {
|
||||
ctx.lineTo(W, H - RT)
|
||||
ctx.arcTo(W, H, W - RT, H, RT)
|
||||
ctx.lineTo(RT, H)
|
||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
||||
}
|
||||
|
||||
ctx.lineTo(0, RT)
|
||||
ctx.arcTo(0, 0, RT, 0, RT)
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
ctx.translate(W, H_raw)
|
||||
ctx.rotate(Math.PI)
|
||||
} else if (isLeft) {
|
||||
ctx.translate(0, W)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
} else if (isRight) {
|
||||
ctx.translate(H_raw, 0)
|
||||
ctx.rotate(Math.PI / 2)
|
||||
}
|
||||
|
||||
drawTopPath()
|
||||
ctx.restore()
|
||||
|
||||
ctx.fillStyle = barWindow._bgColor
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: barTint
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
|
||||
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onAlphaTintChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: barWindow
|
||||
function on_BgColorChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
||||
const R = wing
|
||||
const RT = rt
|
||||
const H = H_raw - (R > 0 ? R : 0)
|
||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
|
||||
function drawTopPath() {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(RT, 0)
|
||||
ctx.lineTo(W - RT, 0)
|
||||
ctx.arcTo(W, 0, W, RT, RT)
|
||||
ctx.lineTo(W, H)
|
||||
|
||||
if (R > 0) {
|
||||
ctx.lineTo(W, H + R)
|
||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
||||
ctx.lineTo(R, H)
|
||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
||||
ctx.lineTo(0, H + R)
|
||||
} else {
|
||||
ctx.lineTo(W, H - RT)
|
||||
ctx.arcTo(W, H, W - RT, H, RT)
|
||||
ctx.lineTo(RT, H)
|
||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
||||
}
|
||||
|
||||
ctx.lineTo(0, RT)
|
||||
ctx.arcTo(0, 0, RT, 0, RT)
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
ctx.translate(W, H_raw)
|
||||
ctx.rotate(Math.PI)
|
||||
} else if (isLeft) {
|
||||
ctx.translate(0, W)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
} else if (isRight) {
|
||||
ctx.translate(H_raw, 0)
|
||||
ctx.rotate(Math.PI / 2)
|
||||
}
|
||||
|
||||
drawTopPath()
|
||||
ctx.restore()
|
||||
|
||||
ctx.fillStyle = Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, alphaTint)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: barBorder
|
||||
anchors.fill: parent
|
||||
visible: SettingsData.dankBarBorderEnabled
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
property bool borderEnabled: SettingsData.dankBarBorderEnabled
|
||||
|
||||
antialiasing: rt > 0 || wing > 0
|
||||
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onBorderEnabledChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceTextChanged() { root.requestRepaint() }
|
||||
function onPrimaryChanged() { root.requestRepaint() }
|
||||
function onSecondaryChanged() { root.requestRepaint() }
|
||||
function onOutlineChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarBorderColorChanged() { root.requestRepaint() }
|
||||
function onDankBarBorderOpacityChanged() { root.requestRepaint() }
|
||||
function onDankBarBorderThicknessChanged() { root.requestRepaint() }
|
||||
function onDankBarSpacingChanged() { root.requestRepaint() }
|
||||
function onDankBarSquareCornersChanged() { root.requestRepaint() }
|
||||
function onDankBarTransparencyChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
if (!borderEnabled) return
|
||||
|
||||
const ctx = getContext("2d")
|
||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
||||
const R = wing
|
||||
const RT = rt
|
||||
const H = H_raw - (R > 0 ? R : 0)
|
||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
|
||||
const spacing = SettingsData.dankBarSpacing
|
||||
const hasEdgeGap = spacing > 0 || RT > 0
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
ctx.translate(W, H_raw)
|
||||
ctx.rotate(Math.PI)
|
||||
} else if (isLeft) {
|
||||
ctx.translate(0, W)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
} else if (isRight) {
|
||||
ctx.translate(H_raw, 0)
|
||||
ctx.rotate(Math.PI / 2)
|
||||
}
|
||||
|
||||
const uiThickness = Math.max(1, SettingsData.dankBarBorderThickness ?? 1)
|
||||
const devThickness = Math.max(1, Math.round(Theme.px(uiThickness, dpr)))
|
||||
|
||||
const key = SettingsData.dankBarBorderColor || "surfaceText"
|
||||
const base = (key === "surfaceText") ? Theme.surfaceText
|
||||
: (key === "primary") ? Theme.primary
|
||||
: Theme.secondary
|
||||
const color = Theme.withAlpha(base, SettingsData.dankBarBorderOpacity ?? 1.0)
|
||||
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
ctx.fillStyle = color
|
||||
|
||||
function drawTopBorder() {
|
||||
if (!hasEdgeGap) {
|
||||
ctx.beginPath()
|
||||
ctx.rect(0, H - devThickness, W, devThickness)
|
||||
ctx.fill()
|
||||
} else {
|
||||
const thk = devThickness
|
||||
const RTi = Math.max(0, RT - thk)
|
||||
const Ri = Math.max(0, R - thk)
|
||||
|
||||
ctx.beginPath()
|
||||
|
||||
if (R > 0 && Ri > 0) {
|
||||
ctx.moveTo(RT, 0)
|
||||
ctx.lineTo(W - RT, 0)
|
||||
ctx.arcTo(W, 0, W, RT, RT)
|
||||
ctx.lineTo(W, H)
|
||||
ctx.lineTo(W, H + R)
|
||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
||||
ctx.lineTo(R, H)
|
||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
||||
ctx.lineTo(0, H + R)
|
||||
ctx.lineTo(0, RT)
|
||||
ctx.arcTo(0, 0, RT, 0, RT)
|
||||
ctx.closePath()
|
||||
|
||||
ctx.moveTo(RT, thk)
|
||||
ctx.arcTo(thk, thk, thk, RT, RTi)
|
||||
ctx.lineTo(thk, H + R)
|
||||
ctx.arc(R, H + R, Ri, -Math.PI, -Math.PI / 2, false)
|
||||
ctx.lineTo(W - R, H + thk)
|
||||
ctx.arc(W - R, H + R, Ri, -Math.PI / 2, 0, false)
|
||||
ctx.lineTo(W - thk, H + R)
|
||||
ctx.lineTo(W - thk, RT)
|
||||
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
|
||||
ctx.lineTo(RT, thk)
|
||||
ctx.closePath()
|
||||
} else {
|
||||
ctx.moveTo(RT, 0)
|
||||
ctx.lineTo(W - RT, 0)
|
||||
ctx.arcTo(W, 0, W, RT, RT)
|
||||
ctx.lineTo(W, H - RT)
|
||||
ctx.arcTo(W, H, W - RT, H, RT)
|
||||
ctx.lineTo(RT, H)
|
||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
||||
ctx.lineTo(0, RT)
|
||||
ctx.arcTo(0, 0, RT, 0, RT)
|
||||
ctx.closePath()
|
||||
|
||||
ctx.moveTo(RT, thk)
|
||||
ctx.arcTo(thk, thk, thk, RT, RTi)
|
||||
ctx.lineTo(thk, H - RT)
|
||||
ctx.arcTo(thk, H - thk, RT, H - thk, RTi)
|
||||
ctx.lineTo(W - RT, H - thk)
|
||||
ctx.arcTo(W - thk, H - thk, W - thk, H - RT, RTi)
|
||||
ctx.lineTo(W - thk, RT)
|
||||
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
|
||||
ctx.lineTo(RT, thk)
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
ctx.fill("evenodd")
|
||||
}
|
||||
}
|
||||
|
||||
drawTopBorder()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,499 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var widgetsModel: null
|
||||
property var components: null
|
||||
property bool noBackground: false
|
||||
required property var axis
|
||||
property string section: "center"
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property bool overrideAxisLayout: false
|
||||
property bool forceVerticalLayout: false
|
||||
|
||||
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
|
||||
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
|
||||
|
||||
property var centerWidgets: []
|
||||
property int totalWidgets: 0
|
||||
property real totalSize: 0
|
||||
|
||||
function updateLayout() {
|
||||
if ((isVertical ? height : width) <= 0 || !visible) {
|
||||
return
|
||||
}
|
||||
|
||||
centerWidgets = []
|
||||
totalWidgets = 0
|
||||
totalSize = 0
|
||||
|
||||
let configuredWidgets = 0
|
||||
let configuredMiddleWidget = null
|
||||
let configuredLeftWidget = null
|
||||
let configuredRightWidget = null
|
||||
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
const item = centerRepeater.itemAt(i)
|
||||
if (item && getWidgetVisible(item.widgetId)) {
|
||||
configuredWidgets++
|
||||
}
|
||||
}
|
||||
|
||||
const isOddConfigured = configuredWidgets % 2 === 1
|
||||
const configuredMiddlePos = Math.floor(configuredWidgets / 2)
|
||||
const configuredLeftPos = isOddConfigured ? -1 : ((configuredWidgets / 2) - 1)
|
||||
const configuredRightPos = isOddConfigured ? -1 : (configuredWidgets / 2)
|
||||
let currentConfigIndex = 0
|
||||
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
const item = centerRepeater.itemAt(i)
|
||||
if (item && getWidgetVisible(item.widgetId)) {
|
||||
if (isOddConfigured && currentConfigIndex === configuredMiddlePos && item.active && item.item) {
|
||||
configuredMiddleWidget = item.item
|
||||
}
|
||||
if (!isOddConfigured && currentConfigIndex === configuredLeftPos && item.active && item.item) {
|
||||
configuredLeftWidget = item.item
|
||||
}
|
||||
if (!isOddConfigured && currentConfigIndex === configuredRightPos && item.active && item.item) {
|
||||
configuredRightWidget = item.item
|
||||
}
|
||||
if (item.active && item.item) {
|
||||
centerWidgets.push(item.item)
|
||||
totalWidgets++
|
||||
totalSize += isVertical ? item.item.height : item.item.width
|
||||
}
|
||||
currentConfigIndex++
|
||||
}
|
||||
}
|
||||
|
||||
if (totalWidgets === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (totalWidgets > 1) {
|
||||
totalSize += spacing * (totalWidgets - 1)
|
||||
}
|
||||
|
||||
positionWidgets(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget)
|
||||
}
|
||||
|
||||
function positionWidgets(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget) {
|
||||
const parentCenter = (isVertical ? height : width) / 2
|
||||
const isOddConfigured = configuredWidgets % 2 === 1
|
||||
|
||||
centerWidgets.forEach(widget => {
|
||||
if (isVertical) {
|
||||
widget.anchors.verticalCenter = undefined
|
||||
} else {
|
||||
widget.anchors.horizontalCenter = undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (isOddConfigured && configuredMiddleWidget) {
|
||||
const middleWidget = configuredMiddleWidget
|
||||
const middleIndex = centerWidgets.indexOf(middleWidget)
|
||||
const middleSize = isVertical ? middleWidget.height : middleWidget.width
|
||||
|
||||
if (isVertical) {
|
||||
middleWidget.y = parentCenter - (middleSize / 2)
|
||||
} else {
|
||||
middleWidget.x = parentCenter - (middleSize / 2)
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? middleWidget.y : middleWidget.x
|
||||
for (var i = middleIndex - 1; i >= 0; i--) {
|
||||
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
|
||||
for (var i = middleIndex + 1; i < totalWidgets; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
}
|
||||
} else {
|
||||
if (totalWidgets === 1) {
|
||||
const widget = centerWidgets[0]
|
||||
const size = isVertical ? widget.height : widget.width
|
||||
if (isVertical) {
|
||||
widget.y = parentCenter - (size / 2)
|
||||
} else {
|
||||
widget.x = parentCenter - (size / 2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!configuredLeftWidget || !configuredRightWidget) {
|
||||
if (totalWidgets % 2 === 1) {
|
||||
const middleIndex = Math.floor(totalWidgets / 2)
|
||||
const middleWidget = centerWidgets[middleIndex]
|
||||
|
||||
if (!middleWidget) {
|
||||
return
|
||||
}
|
||||
|
||||
const middleSize = isVertical ? middleWidget.height : middleWidget.width
|
||||
|
||||
if (isVertical) {
|
||||
middleWidget.y = parentCenter - (middleSize / 2)
|
||||
} else {
|
||||
middleWidget.x = parentCenter - (middleSize / 2)
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? middleWidget.y : middleWidget.x
|
||||
for (var i = middleIndex - 1; i >= 0; i--) {
|
||||
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
|
||||
for (var i = middleIndex + 1; i < totalWidgets; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
}
|
||||
} else {
|
||||
const leftIndex = (totalWidgets / 2) - 1
|
||||
const rightIndex = totalWidgets / 2
|
||||
const fallbackLeft = centerWidgets[leftIndex]
|
||||
const fallbackRight = centerWidgets[rightIndex]
|
||||
|
||||
if (!fallbackLeft || !fallbackRight) {
|
||||
return
|
||||
}
|
||||
|
||||
const halfSpacing = spacing / 2
|
||||
const leftSize = isVertical ? fallbackLeft.height : fallbackLeft.width
|
||||
|
||||
if (isVertical) {
|
||||
fallbackLeft.y = parentCenter - halfSpacing - leftSize
|
||||
fallbackRight.y = parentCenter + halfSpacing
|
||||
} else {
|
||||
fallbackLeft.x = parentCenter - halfSpacing - leftSize
|
||||
fallbackRight.x = parentCenter + halfSpacing
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? fallbackLeft.y : fallbackLeft.x
|
||||
for (var i = leftIndex - 1; i >= 0; i--) {
|
||||
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? fallbackRight.y + fallbackRight.height : fallbackRight.x + fallbackRight.width)
|
||||
for (var i = rightIndex + 1; i < totalWidgets; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const leftWidget = configuredLeftWidget
|
||||
const rightWidget = configuredRightWidget
|
||||
const leftIndex = centerWidgets.indexOf(leftWidget)
|
||||
const rightIndex = centerWidgets.indexOf(rightWidget)
|
||||
const halfSpacing = spacing / 2
|
||||
const leftSize = isVertical ? leftWidget.height : leftWidget.width
|
||||
|
||||
if (isVertical) {
|
||||
leftWidget.y = parentCenter - halfSpacing - leftSize
|
||||
rightWidget.y = parentCenter + halfSpacing
|
||||
} else {
|
||||
leftWidget.x = parentCenter - halfSpacing - leftSize
|
||||
rightWidget.x = parentCenter + halfSpacing
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? leftWidget.y : leftWidget.x
|
||||
for (var i = leftIndex - 1; i >= 0; i--) {
|
||||
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
|
||||
for (var i = rightIndex + 1; i < totalWidgets; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetVisible(widgetId) {
|
||||
const widgetVisibility = {
|
||||
"cpuUsage": DgopService.dgopAvailable,
|
||||
"memUsage": DgopService.dgopAvailable,
|
||||
"cpuTemp": DgopService.dgopAvailable,
|
||||
"gpuTemp": DgopService.dgopAvailable,
|
||||
"network_speed_monitor": DgopService.dgopAvailable
|
||||
}
|
||||
return widgetVisibility[widgetId] ?? true
|
||||
}
|
||||
|
||||
function getWidgetComponent(widgetId) {
|
||||
// Build dynamic component map including plugins
|
||||
let baseMap = {
|
||||
"launcherButton": "launcherButtonComponent",
|
||||
"workspaceSwitcher": "workspaceSwitcherComponent",
|
||||
"focusedWindow": "focusedWindowComponent",
|
||||
"runningApps": "runningAppsComponent",
|
||||
"clock": "clockComponent",
|
||||
"music": "mediaComponent",
|
||||
"weather": "weatherComponent",
|
||||
"systemTray": "systemTrayComponent",
|
||||
"privacyIndicator": "privacyIndicatorComponent",
|
||||
"clipboard": "clipboardComponent",
|
||||
"cpuUsage": "cpuUsageComponent",
|
||||
"memUsage": "memUsageComponent",
|
||||
"diskUsage": "diskUsageComponent",
|
||||
"cpuTemp": "cpuTempComponent",
|
||||
"gpuTemp": "gpuTempComponent",
|
||||
"notificationButton": "notificationButtonComponent",
|
||||
"battery": "batteryComponent",
|
||||
"controlCenterButton": "controlCenterButtonComponent",
|
||||
"idleInhibitor": "idleInhibitorComponent",
|
||||
"spacer": "spacerComponent",
|
||||
"separator": "separatorComponent",
|
||||
"network_speed_monitor": "networkComponent",
|
||||
"keyboard_layout_name": "keyboardLayoutNameComponent",
|
||||
"vpn": "vpnComponent",
|
||||
"notepadButton": "notepadButtonComponent",
|
||||
"colorPicker": "colorPickerComponent",
|
||||
"systemUpdate": "systemUpdateComponent"
|
||||
}
|
||||
|
||||
// For built-in components, get from components property
|
||||
const componentKey = baseMap[widgetId]
|
||||
if (componentKey && root.components[componentKey]) {
|
||||
return root.components[componentKey]
|
||||
}
|
||||
|
||||
// For plugin components, get from PluginService
|
||||
var parts = widgetId.split(":")
|
||||
var pluginId = parts[0]
|
||||
let pluginComponents = PluginService.getWidgetComponents()
|
||||
return pluginComponents[pluginId] || null
|
||||
}
|
||||
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
anchors.centerIn: parent
|
||||
|
||||
Timer {
|
||||
id: layoutTimer
|
||||
interval: 0
|
||||
repeat: false
|
||||
onTriggered: root.updateLayout()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
layoutTimer.restart()
|
||||
}
|
||||
|
||||
onWidthChanged: {
|
||||
if (width > 0) {
|
||||
layoutTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onHeightChanged: {
|
||||
if (height > 0) {
|
||||
layoutTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && (isVertical ? height : width) > 0) {
|
||||
layoutTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: centerRepeater
|
||||
model: root.widgetsModel
|
||||
|
||||
|
||||
Loader {
|
||||
property string widgetId: model.widgetId
|
||||
property var widgetData: model
|
||||
property int spacerSize: model.size || 20
|
||||
|
||||
anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined
|
||||
anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined
|
||||
active: root.getWidgetVisible(model.widgetId) && (model.widgetId !== "music" || MprisController.activePlayer !== null)
|
||||
sourceComponent: root.getWidgetComponent(model.widgetId)
|
||||
opacity: (model.enabled !== false) ? 1 : 0
|
||||
asynchronous: false
|
||||
|
||||
onLoaded: {
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
item.widthChanged.connect(() => layoutTimer.restart())
|
||||
item.heightChanged.connect(() => layoutTimer.restart())
|
||||
if (root.axis && "axis" in item) {
|
||||
item.axis = Qt.binding(() => root.axis)
|
||||
}
|
||||
if (root.axis && "isVertical" in item) {
|
||||
try {
|
||||
item.isVertical = Qt.binding(() => root.axis.isVertical)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Inject properties for plugin widgets
|
||||
if ("section" in item) {
|
||||
item.section = root.section
|
||||
}
|
||||
if ("parentScreen" in item) {
|
||||
item.parentScreen = Qt.binding(() => root.parentScreen)
|
||||
}
|
||||
if ("widgetThickness" in item) {
|
||||
item.widgetThickness = Qt.binding(() => root.widgetThickness)
|
||||
}
|
||||
if ("barThickness" in item) {
|
||||
item.barThickness = Qt.binding(() => root.barThickness)
|
||||
}
|
||||
if ("sectionSpacing" in item) {
|
||||
item.sectionSpacing = Qt.binding(() => root.spacing)
|
||||
}
|
||||
|
||||
if ("isFirst" in item) {
|
||||
item.isFirst = Qt.binding(() => {
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
const checkItem = centerRepeater.itemAt(i)
|
||||
if (checkItem && checkItem.active && checkItem.item) {
|
||||
return checkItem.item === item
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
if ("isLast" in item) {
|
||||
item.isLast = Qt.binding(() => {
|
||||
for (var i = centerRepeater.count - 1; i >= 0; i--) {
|
||||
const checkItem = centerRepeater.itemAt(i)
|
||||
if (checkItem && checkItem.active && checkItem.item) {
|
||||
return checkItem.item === item
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
if ("isLeftBarEdge" in item) {
|
||||
item.isLeftBarEdge = false
|
||||
}
|
||||
if ("isRightBarEdge" in item) {
|
||||
item.isRightBarEdge = false
|
||||
}
|
||||
if ("isTopBarEdge" in item) {
|
||||
item.isTopBarEdge = false
|
||||
}
|
||||
if ("isBottomBarEdge" in item) {
|
||||
item.isBottomBarEdge = false
|
||||
}
|
||||
|
||||
if (item.pluginService !== undefined) {
|
||||
var parts = model.widgetId.split(":")
|
||||
var pluginId = parts[0]
|
||||
var variantId = parts.length > 1 ? parts[1] : null
|
||||
|
||||
if (item.pluginId !== undefined) {
|
||||
item.pluginId = pluginId
|
||||
}
|
||||
if (item.variantId !== undefined) {
|
||||
item.variantId = variantId
|
||||
}
|
||||
if (item.variantData !== undefined && variantId) {
|
||||
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
|
||||
}
|
||||
item.pluginService = PluginService
|
||||
}
|
||||
|
||||
if (item.popoutService !== undefined) {
|
||||
item.popoutService = PopoutService
|
||||
}
|
||||
|
||||
layoutTimer.restart()
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
layoutTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: widgetsModel
|
||||
function onCountChanged() {
|
||||
layoutTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for plugin changes and refresh components
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded(pluginId) {
|
||||
// Force refresh of component lookups
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
var item = centerRepeater.itemAt(i)
|
||||
if (item && item.widgetId.startsWith(pluginId)) {
|
||||
item.sourceComponent = root.getWidgetComponent(item.widgetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
function onPluginUnloaded(pluginId) {
|
||||
// Force refresh of component lookups
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
var item = centerRepeater.itemAt(i)
|
||||
if (item && item.widgetId.startsWith(pluginId)) {
|
||||
item.sourceComponent = root.getWidgetComponent(item.widgetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,439 +0,0 @@
|
||||
// No external details import; content inlined for consistency
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:vpn"
|
||||
|
||||
Ref {
|
||||
service: DMSNetworkService
|
||||
}
|
||||
|
||||
property bool wasVisible: false
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible && !wasVisible) {
|
||||
DMSNetworkService.getState()
|
||||
}
|
||||
wasVisible = shouldBeVisible
|
||||
}
|
||||
|
||||
property var triggerScreen: null
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerX = x;
|
||||
triggerY = y;
|
||||
triggerWidth = width;
|
||||
triggerSection = section;
|
||||
triggerScreen = screen;
|
||||
}
|
||||
|
||||
popupWidth: 360
|
||||
popupHeight: Math.min(Screen.height - 100, contentLoader.item ? contentLoader.item.implicitHeight : 260)
|
||||
triggerX: Screen.width - 380 - Theme.spacingL
|
||||
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
|
||||
triggerWidth: 70
|
||||
positioning: ""
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: false
|
||||
visible: shouldBeVisible
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: content
|
||||
|
||||
implicitHeight: contentColumn.height + Theme.spacingL * 2
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
focus: true
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Outer subtle shadow rings to match BatteryPopout
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
color: "transparent"
|
||||
radius: parent.radius + 3
|
||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
||||
border.width: 0
|
||||
z: -3
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -2
|
||||
color: "transparent"
|
||||
radius: parent.radius + 2
|
||||
border.color: Theme.shadowMedium
|
||||
border.width: 0
|
||||
z: -2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 0
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("VPN Connections")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Close button (matches BatteryPopout)
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: Theme.iconSize - 4
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: root.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Inlined VPN details
|
||||
Rectangle {
|
||||
id: vpnDetail
|
||||
|
||||
width: parent.width
|
||||
implicitHeight: detailsColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 0
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: detailsColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
RowLayout {
|
||||
spacing: Theme.spacingS
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DMSNetworkService.connected) {
|
||||
return "Active: None";
|
||||
}
|
||||
|
||||
const names = DMSNetworkService.activeNames || [];
|
||||
if (names.length <= 1) {
|
||||
return "Active: " + (names[0] || "VPN");
|
||||
}
|
||||
|
||||
return "Active: " + names[0] + " +" + (names.length - 1);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: parent.width - 140
|
||||
}
|
||||
|
||||
// Removed Quick Connect for clarity
|
||||
Item {
|
||||
width: 1
|
||||
height: 1
|
||||
}
|
||||
|
||||
// Disconnect all (shown only when any active)
|
||||
Rectangle {
|
||||
height: 28
|
||||
radius: 14
|
||||
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
|
||||
visible: DMSNetworkService.connected
|
||||
width: 130
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
border.width: 0
|
||||
border.color: Theme.outlineLight
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "link_off"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disconnect")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: discAllArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onClicked: DMSNetworkService.disconnectAllActive()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
width: parent.width
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
width: parent.width
|
||||
height: 160
|
||||
contentHeight: listCol.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: listCol
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: DMSNetworkService.profiles.length === 0 ? 120 : 0
|
||||
visible: height > 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "playlist_remove"
|
||||
size: 36
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No VPN profiles found")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Add a VPN in NetworkManager")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: DMSNetworkService.profiles
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
width: parent ? parent.width : 300
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
|
||||
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
|
||||
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
|
||||
size: Theme.iconSize - 4
|
||||
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (modelData.type === "wireguard") {
|
||||
return "WireGuard";
|
||||
}
|
||||
|
||||
const svc = modelData.serviceType || "";
|
||||
if (svc.indexOf("openvpn") !== -1) {
|
||||
return "OpenVPN";
|
||||
}
|
||||
|
||||
if (svc.indexOf("wireguard") !== -1) {
|
||||
return "WireGuard (plugin)";
|
||||
}
|
||||
|
||||
if (svc.indexOf("openconnect") !== -1) {
|
||||
return "OpenConnect";
|
||||
}
|
||||
|
||||
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1) {
|
||||
return "Fortinet";
|
||||
}
|
||||
|
||||
if (svc.indexOf("strongswan") !== -1) {
|
||||
return "IPsec (strongSwan)";
|
||||
}
|
||||
|
||||
if (svc.indexOf("libreswan") !== -1) {
|
||||
return "IPsec (Libreswan)";
|
||||
}
|
||||
|
||||
if (svc.indexOf("l2tp") !== -1) {
|
||||
return "L2TP/IPsec";
|
||||
}
|
||||
|
||||
if (svc.indexOf("pptp") !== -1) {
|
||||
return "PPTP";
|
||||
}
|
||||
|
||||
if (svc.indexOf("vpnc") !== -1) {
|
||||
return "Cisco (vpnc)";
|
||||
}
|
||||
|
||||
if (svc.indexOf("sstp") !== -1) {
|
||||
return "SSTP";
|
||||
}
|
||||
|
||||
if (svc) {
|
||||
const parts = svc.split('.');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
return "VPN";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onClicked: DMSNetworkService.toggle(modelData.uuid)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
height: 1
|
||||
width: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
readonly property bool hasActiveMedia: activePlayer !== null
|
||||
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
width: 20
|
||||
height: Theme.iconSize
|
||||
|
||||
Loader {
|
||||
active: isPlaying
|
||||
|
||||
sourceComponent: Component {
|
||||
Ref {
|
||||
service: CavaService
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fallbackTimer
|
||||
|
||||
running: !CavaService.cavaAvailable && isPlaying
|
||||
interval: 256
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25];
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 1.5
|
||||
|
||||
Repeater {
|
||||
model: 6
|
||||
|
||||
Rectangle {
|
||||
width: 2
|
||||
height: {
|
||||
if (root.isPlaying && CavaService.values.length > index) {
|
||||
const rawLevel = CavaService.values[index] || 0;
|
||||
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
|
||||
const maxHeight = Theme.iconSize - 2;
|
||||
const minHeight = 3;
|
||||
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
radius: 1.5
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.standardDecel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
property bool isActive: false
|
||||
property var popoutTarget: null
|
||||
property var widgetData: null
|
||||
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
|
||||
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
|
||||
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : controlIndicators.implicitWidth
|
||||
implicitHeight: root.isVerticalOrientation ? controlColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
|
||||
|
||||
Column {
|
||||
id: controlColumn
|
||||
visible: root.isVerticalOrientation
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return "sync"
|
||||
}
|
||||
|
||||
const status = NetworkService.networkStatus
|
||||
if (status === "ethernet") {
|
||||
return "lan"
|
||||
}
|
||||
|
||||
if (status === "vpn") {
|
||||
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
|
||||
}
|
||||
|
||||
return NetworkService.wifiSignalIcon
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "bluetooth"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: audioIconV.implicitWidth + 4
|
||||
height: audioIconV.implicitHeight + 4
|
||||
color: "transparent"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.showAudioIcon
|
||||
|
||||
DankIcon {
|
||||
id: audioIconV
|
||||
|
||||
name: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
|
||||
return "volume_off"
|
||||
} else if (AudioService.sink.audio.volume * 100 < 33) {
|
||||
return "volume_down"
|
||||
} else {
|
||||
return "volume_up"
|
||||
}
|
||||
}
|
||||
return "volume_up"
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: function(wheelEvent) {
|
||||
let delta = wheelEvent.angleDelta.y
|
||||
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
|
||||
let newVolume
|
||||
if (delta > 0) {
|
||||
newVolume = Math.min(100, currentVolume + 5)
|
||||
} else {
|
||||
newVolume = Math.max(0, currentVolume - 5)
|
||||
}
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false
|
||||
AudioService.sink.audio.volume = newVolume / 100
|
||||
}
|
||||
wheelEvent.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controlIndicators
|
||||
visible: !root.isVerticalOrientation
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
id: networkIcon
|
||||
|
||||
name: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return "sync"
|
||||
}
|
||||
|
||||
const status = NetworkService.networkStatus
|
||||
if (status === "ethernet") {
|
||||
return "lan"
|
||||
}
|
||||
|
||||
if (status === "vpn") {
|
||||
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
|
||||
}
|
||||
|
||||
return NetworkService.wifiSignalIcon
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: bluetoothIcon
|
||||
|
||||
name: "bluetooth"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: audioIcon.implicitWidth + 4
|
||||
height: audioIcon.implicitHeight + 4
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showAudioIcon
|
||||
|
||||
DankIcon {
|
||||
id: audioIcon
|
||||
|
||||
name: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
|
||||
return "volume_off";
|
||||
} else if (AudioService.sink.audio.volume * 100 < 33) {
|
||||
return "volume_down";
|
||||
} else {
|
||||
return "volume_up";
|
||||
}
|
||||
}
|
||||
return "volume_up";
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: audioWheelArea
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: function(wheelEvent) {
|
||||
let delta = wheelEvent.angleDelta.y;
|
||||
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
|
||||
let newVolume;
|
||||
if (delta > 0) {
|
||||
newVolume = Math.min(100, currentVolume + 5);
|
||||
} else {
|
||||
newVolume = Math.max(0, currentVolume - 5);
|
||||
}
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
wheelEvent.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "mic"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: false
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: -root.leftMargin
|
||||
y: -root.topMargin
|
||||
width: root.width + root.leftMargin + root.rightMargin
|
||||
height: root.height + root.topMargin + root.bottomMargin
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onPressed: {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
property bool compactMode: SettingsData.keyboardLayoutNameCompactMode
|
||||
property string currentLayout: CompositorService.isNiri ? NiriService.getCurrentKeyboardLayoutName() : ""
|
||||
property string hyprlandKeyboard: ""
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth
|
||||
implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
visible: root.isVerticalOrientation
|
||||
anchors.centerIn: parent
|
||||
spacing: 1
|
||||
|
||||
DankIcon {
|
||||
name: "keyboard"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!root.currentLayout) return ""
|
||||
const parts = root.currentLayout.split(" ")
|
||||
if (parts.length > 0) {
|
||||
return parts[0].substring(0, 2).toUpperCase()
|
||||
}
|
||||
return root.currentLayout.substring(0, 2).toUpperCase()
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
visible: !root.isVerticalOrientation
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: root.currentLayout
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
z: 1
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.cycleKeyboardLayout()
|
||||
} else if (CompositorService.isHyprland) {
|
||||
Quickshell.execDetached([
|
||||
"hyprctl",
|
||||
"switchxkblayout",
|
||||
root.hyprlandKeyboard,
|
||||
"next"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService.isHyprland ? Hyprland : null
|
||||
enabled: CompositorService.isHyprland
|
||||
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "activelayout") {
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (CompositorService.isHyprland) {
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
function updateLayout() {
|
||||
if (CompositorService.isHyprland) {
|
||||
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) {
|
||||
const layout = mainKeyboard.layout
|
||||
const variant = mainKeyboard.variant
|
||||
const index = mainKeyboard.active_layout_index
|
||||
|
||||
if (root.compactMode && layout && variant && index !== undefined) {
|
||||
const layouts = mainKeyboard.layout.split(",")
|
||||
const variants = mainKeyboard.variant.split(",")
|
||||
const index = mainKeyboard.active_layout_index
|
||||
|
||||
if (layouts[index] && variants[index] !== undefined) {
|
||||
if (variants[index] === "") {
|
||||
root.currentLayout = layouts[index]
|
||||
} else {
|
||||
root.currentLayout = layouts[index] + "-" + variants[index]
|
||||
}
|
||||
} else {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
} else if (mainKeyboard && mainKeyboard.active_keymap) {
|
||||
root.currentLayout = mainKeyboard.active_keymap
|
||||
} else {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
} else {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
} catch (e) {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,798 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool isVertical: axis?.isVertical ?? false
|
||||
property var axis: null
|
||||
property string section: "left"
|
||||
property var parentScreen
|
||||
property var hoveredItem: null
|
||||
property var topBar: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
|
||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
||||
property int _desktopEntriesUpdateTrigger: 0
|
||||
property int _toplevelsUpdateTrigger: 0
|
||||
|
||||
readonly property var sortedToplevels: {
|
||||
_toplevelsUpdateTrigger
|
||||
const toplevels = CompositorService.sortedToplevels
|
||||
if (!toplevels || toplevels.length === 0) return []
|
||||
|
||||
if (SettingsData.runningAppsCurrentWorkspace) {
|
||||
return CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || []
|
||||
}
|
||||
return toplevels
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onToplevelsChanged() {
|
||||
_toplevelsUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
_desktopEntriesUpdateTrigger++
|
||||
}
|
||||
}
|
||||
readonly property var groupedWindows: {
|
||||
if (!SettingsData.runningAppsGroupByApp) {
|
||||
return []
|
||||
}
|
||||
try {
|
||||
if (!sortedToplevels || sortedToplevels.length === 0) {
|
||||
return []
|
||||
}
|
||||
const appGroups = new Map()
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
if (!toplevel)
|
||||
return
|
||||
const appId = toplevel?.appId || "unknown"
|
||||
if (!appGroups.has(appId)) {
|
||||
appGroups.set(appId, {
|
||||
"appId": appId,
|
||||
"windows": []
|
||||
})
|
||||
}
|
||||
appGroups.get(appId).windows.push({
|
||||
"toplevel": toplevel,
|
||||
"windowId": index,
|
||||
"windowTitle": toplevel?.title || "(Unnamed)"
|
||||
})
|
||||
})
|
||||
return Array.from(appGroups.values())
|
||||
} catch (e) {
|
||||
console.error("RunningApps: groupedWindows error:", e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
readonly property int windowCount: SettingsData.runningAppsGroupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
|
||||
readonly property int calculatedSize: {
|
||||
if (windowCount === 0) {
|
||||
return 0
|
||||
}
|
||||
if (SettingsData.runningAppsCompactMode) {
|
||||
return windowCount * 24 + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
|
||||
} else {
|
||||
return windowCount * (24 + Theme.spacingXS + 120) + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
|
||||
}
|
||||
}
|
||||
|
||||
width: isVertical ? barThickness : calculatedSize
|
||||
height: isVertical ? calculatedSize : barThickness
|
||||
visible: windowCount > 0
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: visualBackground
|
||||
width: root.isVertical ? root.widgetThickness : root.calculatedSize
|
||||
height: root.isVertical ? root.calculatedSize : root.widgetThickness
|
||||
anchors.centerIn: parent
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
clip: false
|
||||
color: {
|
||||
if (windowCount === 0) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
if (SettingsData.dankBarNoBackground) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
const baseColor = Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
property real scrollAccumulator: 0
|
||||
property real touchpadThreshold: 500
|
||||
|
||||
onWheel: wheel => {
|
||||
const deltaY = wheel.angleDelta.y
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
||||
|
||||
const windows = root.sortedToplevels
|
||||
if (windows.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMouseWheel) {
|
||||
// Direct mouse wheel action
|
||||
let currentIndex = -1
|
||||
for (var 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) % windows.length
|
||||
}
|
||||
} else {
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = windows.length - 1
|
||||
} else {
|
||||
nextIndex = (currentIndex - 1 + windows.length) % windows.length
|
||||
}
|
||||
}
|
||||
|
||||
const nextWindow = windows[nextIndex]
|
||||
if (nextWindow) {
|
||||
nextWindow.activate()
|
||||
}
|
||||
} else {
|
||||
// Touchpad - accumulate small deltas
|
||||
scrollAccumulator += deltaY
|
||||
|
||||
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
|
||||
let currentIndex = -1
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
if (windows[i].activated) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let nextIndex
|
||||
if (scrollAccumulator < 0) {
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = 0
|
||||
} else {
|
||||
nextIndex = (currentIndex + 1) % windows.length
|
||||
}
|
||||
} else {
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = windows.length - 1
|
||||
} else {
|
||||
nextIndex = (currentIndex - 1 + windows.length) % windows.length
|
||||
}
|
||||
}
|
||||
|
||||
const nextWindow = windows[nextIndex]
|
||||
if (nextWindow) {
|
||||
nextWindow.activate()
|
||||
}
|
||||
|
||||
scrollAccumulator = 0
|
||||
}
|
||||
}
|
||||
|
||||
wheel.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: layoutLoader
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: root.isVertical ? columnLayout : rowLayout
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowLayout
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
id: windowRepeater
|
||||
model: ScriptModel {
|
||||
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
|
||||
property bool isGrouped: SettingsData.runningAppsGroupByApp
|
||||
property var groupData: isGrouped ? modelData : null
|
||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
||||
property bool isFocused: toplevelData ? toplevelData.activated : false
|
||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
||||
property var toplevelObject: toplevelData
|
||||
property int windowCount: isGrouped ? modelData.windows.length : 1
|
||||
property string tooltipText: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
let appName = "Unknown"
|
||||
if (appId) {
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
||||
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
|
||||
}
|
||||
if (isGrouped && windowCount > 1) {
|
||||
return appName + " (" + windowCount + " windows)"
|
||||
}
|
||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
||||
}
|
||||
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
|
||||
|
||||
width: visualWidth
|
||||
height: root.barThickness
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: delegateItem.visualWidth
|
||||
height: 24
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
} else {
|
||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
// App icon
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
source: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
if (moddedId.toLowerCase().includes("steam_app")) {
|
||||
return ""
|
||||
}
|
||||
return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
|
||||
}
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
visible: {
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
return moddedId.toLowerCase().includes("steam_app")
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback text if no icon found
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: {
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
||||
return !iconImg.visible && !isSteamApp
|
||||
}
|
||||
text: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
if (!appId) {
|
||||
return "?"
|
||||
}
|
||||
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
||||
if (desktopEntry && desktopEntry.name) {
|
||||
return desktopEntry.name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
return appId.charAt(0).toUpperCase()
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
|
||||
anchors.bottomMargin: -2
|
||||
width: 14
|
||||
height: 14
|
||||
radius: 7
|
||||
color: Theme.primary
|
||||
visible: isGrouped && windowCount > 1
|
||||
z: 10
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: windowCount > 9 ? "9+" : windowCount
|
||||
font.pixelSize: 9
|
||||
color: Theme.surface
|
||||
}
|
||||
}
|
||||
|
||||
// Window title text (only visible in expanded mode)
|
||||
StyledText {
|
||||
anchors.left: iconImg.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !SettingsData.runningAppsCompactMode
|
||||
text: windowTitle
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (isGrouped && windowCount > 1) {
|
||||
let currentIndex = -1
|
||||
for (var i = 0; i < groupData.windows.length; i++) {
|
||||
if (groupData.windows[i].toplevel.activated) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const nextIndex = (currentIndex + 1) % groupData.windows.length
|
||||
groupData.windows[nextIndex].toplevel.activate()
|
||||
} else if (toplevelObject) {
|
||||
toplevelObject.activate()
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
tooltipLoader.active = false
|
||||
|
||||
windowContextMenuLoader.active = true
|
||||
if (windowContextMenuLoader.item) {
|
||||
windowContextMenuLoader.item.currentWindow = toplevelObject
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
||||
const relativeY = globalPos.y - screenY
|
||||
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
||||
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const relativeX = globalPos.x - screenX
|
||||
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
|
||||
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
root.hoveredItem = delegateItem
|
||||
tooltipLoader.active = true
|
||||
if (tooltipLoader.item) {
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.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)
|
||||
const isLeft = root.axis?.edge === "left"
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
|
||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
|
||||
const isBottom = root.axis?.edge === "bottom"
|
||||
const tooltipY = isBottom
|
||||
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
|
||||
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (root.hoveredItem === delegateItem) {
|
||||
root.hoveredItem = null
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
|
||||
tooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: columnLayout
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
id: windowRepeater
|
||||
model: ScriptModel {
|
||||
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
|
||||
property bool isGrouped: SettingsData.runningAppsGroupByApp
|
||||
property var groupData: isGrouped ? modelData : null
|
||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
||||
property bool isFocused: toplevelData ? toplevelData.activated : false
|
||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
||||
property var toplevelObject: toplevelData
|
||||
property int windowCount: isGrouped ? modelData.windows.length : 1
|
||||
property string tooltipText: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
let appName = "Unknown"
|
||||
if (appId) {
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
||||
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
|
||||
}
|
||||
if (isGrouped && windowCount > 1) {
|
||||
return appName + " (" + windowCount + " windows)"
|
||||
}
|
||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
||||
}
|
||||
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
|
||||
|
||||
width: root.barThickness
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: delegateItem.visualWidth
|
||||
height: 24
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
} else {
|
||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
source: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
if (moddedId.toLowerCase().includes("steam_app")) {
|
||||
return ""
|
||||
}
|
||||
return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
|
||||
}
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
visible: {
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
return moddedId.toLowerCase().includes("steam_app")
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: {
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
||||
return !iconImg.visible && !isSteamApp
|
||||
}
|
||||
text: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
if (!appId) {
|
||||
return "?"
|
||||
}
|
||||
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
||||
if (desktopEntry && desktopEntry.name) {
|
||||
return desktopEntry.name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
return appId.charAt(0).toUpperCase()
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
|
||||
anchors.bottomMargin: -2
|
||||
width: 14
|
||||
height: 14
|
||||
radius: 7
|
||||
color: Theme.primary
|
||||
visible: isGrouped && windowCount > 1
|
||||
z: 10
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: windowCount > 9 ? "9+" : windowCount
|
||||
font.pixelSize: 9
|
||||
color: Theme.surface
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: iconImg.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !SettingsData.runningAppsCompactMode
|
||||
text: windowTitle
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (isGrouped && windowCount > 1) {
|
||||
let currentIndex = -1
|
||||
for (var i = 0; i < groupData.windows.length; i++) {
|
||||
if (groupData.windows[i].toplevel.activated) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const nextIndex = (currentIndex + 1) % groupData.windows.length
|
||||
groupData.windows[nextIndex].toplevel.activate()
|
||||
} else if (toplevelObject) {
|
||||
toplevelObject.activate()
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
tooltipLoader.active = false
|
||||
|
||||
windowContextMenuLoader.active = true
|
||||
if (windowContextMenuLoader.item) {
|
||||
windowContextMenuLoader.item.currentWindow = toplevelObject
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
||||
const relativeY = globalPos.y - screenY
|
||||
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
||||
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const relativeX = globalPos.x - screenX
|
||||
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
|
||||
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
root.hoveredItem = delegateItem
|
||||
tooltipLoader.active = true
|
||||
if (tooltipLoader.item) {
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.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)
|
||||
const isLeft = root.axis?.edge === "left"
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
|
||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
|
||||
const isBottom = root.axis?.edge === "bottom"
|
||||
const tooltipY = isBottom
|
||||
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
|
||||
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (root.hoveredItem === delegateItem) {
|
||||
root.hoveredItem = null
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
|
||||
tooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
|
||||
active: false
|
||||
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: windowContextMenuLoader
|
||||
active: false
|
||||
sourceComponent: PanelWindow {
|
||||
id: contextMenuWindow
|
||||
|
||||
property var currentWindow: null
|
||||
property bool isVisible: false
|
||||
property point anchorPos: Qt.point(0, 0)
|
||||
property bool isVertical: false
|
||||
property string edge: "top"
|
||||
|
||||
function showAt(x, y, vertical, barEdge) {
|
||||
screen = root.parentScreen
|
||||
anchorPos = Qt.point(x, y)
|
||||
isVertical = vertical ?? false
|
||||
edge = barEdge ?? "top"
|
||||
isVisible = true
|
||||
visible = true
|
||||
}
|
||||
|
||||
function close() {
|
||||
isVisible = false
|
||||
visible = false
|
||||
windowContextMenuLoader.active = false
|
||||
}
|
||||
|
||||
implicitWidth: 100
|
||||
implicitHeight: 40
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: contextMenuWindow.close()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: {
|
||||
if (contextMenuWindow.isVertical) {
|
||||
if (contextMenuWindow.edge === "left") {
|
||||
return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x)
|
||||
} else {
|
||||
return Math.max(10, contextMenuWindow.anchorPos.x - width)
|
||||
}
|
||||
} else {
|
||||
const left = 10
|
||||
const right = contextMenuWindow.width - width - 10
|
||||
const want = contextMenuWindow.anchorPos.x - width / 2
|
||||
return Math.max(left, Math.min(right, want))
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (contextMenuWindow.isVertical) {
|
||||
const top = 10
|
||||
const bottom = contextMenuWindow.height - height - 10
|
||||
const want = contextMenuWindow.anchorPos.y - height / 2
|
||||
return Math.max(top, Math.min(bottom, want))
|
||||
} else {
|
||||
return contextMenuWindow.anchorPos.y
|
||||
}
|
||||
}
|
||||
width: 100
|
||||
height: 32
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Close")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (contextMenuWindow.currentWindow) {
|
||||
contextMenuWindow.currentWindow.close()
|
||||
}
|
||||
contextMenuWindow.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,112 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
Ref {
|
||||
service: DMSNetworkService
|
||||
}
|
||||
|
||||
property var popoutTarget: null
|
||||
property bool isHovered: clickArea.containsMouse
|
||||
|
||||
signal toggleVpnPopup()
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
|
||||
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
||||
|
||||
DankIcon {
|
||||
id: icon
|
||||
|
||||
name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceText
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clickArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onPressed: {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.toggleVpnPopup();
|
||||
}
|
||||
onEntered: {
|
||||
if (root.parentScreen && !(popoutTarget && popoutTarget.shouldBeVisible)) {
|
||||
tooltipLoader.active = true
|
||||
if (tooltipLoader.item) {
|
||||
let tooltipText = ""
|
||||
if (!DMSNetworkService.connected) {
|
||||
tooltipText = "VPN Disconnected"
|
||||
} else {
|
||||
const names = DMSNetworkService.activeNames || []
|
||||
if (names.length <= 1) {
|
||||
const name = names[0] || ""
|
||||
const maxLength = 25
|
||||
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
|
||||
tooltipText = "VPN Connected • " + displayName
|
||||
} else {
|
||||
const name = names[0]
|
||||
const maxLength = 20
|
||||
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
|
||||
tooltipText = "VPN Connected • " + displayName + " +" + (names.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (root.isVerticalOrientation) {
|
||||
const globalPos = mapToGlobal(width / 2, 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)
|
||||
const isLeft = root.axis?.edge === "left"
|
||||
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
} else {
|
||||
const globalPos = mapToGlobal(width / 2, height)
|
||||
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
|
||||
tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
tooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,950 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.I3
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool isVertical: axis?.isVertical ?? false
|
||||
property var axis: null
|
||||
property string screenName: ""
|
||||
property real widgetHeight: 30
|
||||
property real barThickness: 48
|
||||
property var hyprlandOverviewLoader: null
|
||||
property var parentScreen: null
|
||||
property int _desktopEntriesUpdateTrigger: 0
|
||||
readonly property var sortedToplevels: {
|
||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||
}
|
||||
|
||||
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && ExtWorkspaceService.extWorkspaceAvailable)
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
_desktopEntriesUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
property var currentWorkspace: {
|
||||
if (useExtWorkspace) return getExtWorkspaceActiveWorkspace()
|
||||
|
||||
switch (CompositorService.compositor) {
|
||||
case "niri":
|
||||
return getNiriActiveWorkspace()
|
||||
case "hyprland":
|
||||
return getHyprlandActiveWorkspace()
|
||||
case "dwl":
|
||||
const activeTags = getDwlActiveTags()
|
||||
return activeTags.length > 0 ? activeTags[0] : -1
|
||||
case "sway":
|
||||
return getSwayActiveWorkspace()
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
property var dwlActiveTags: {
|
||||
if (CompositorService.isDwl) {
|
||||
return getDwlActiveTags()
|
||||
}
|
||||
return []
|
||||
}
|
||||
property var workspaceList: {
|
||||
if (useExtWorkspace) {
|
||||
const baseList = getExtWorkspaceWorkspaces()
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
}
|
||||
|
||||
let baseList
|
||||
switch (CompositorService.compositor) {
|
||||
case "niri":
|
||||
baseList = getNiriWorkspaces()
|
||||
break
|
||||
case "hyprland":
|
||||
baseList = getHyprlandWorkspaces()
|
||||
break
|
||||
case "dwl":
|
||||
baseList = getDwlTags()
|
||||
break
|
||||
case "sway":
|
||||
baseList = getSwayWorkspaces()
|
||||
break
|
||||
default:
|
||||
return [1]
|
||||
}
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
}
|
||||
|
||||
function getSwayWorkspaces() {
|
||||
const workspaces = I3.workspaces?.values || []
|
||||
if (workspaces.length === 0) return [{"num": 1}]
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return workspaces.slice().sort((a, b) => a.num - b.num)
|
||||
}
|
||||
|
||||
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
|
||||
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}]
|
||||
}
|
||||
|
||||
function getSwayActiveWorkspace() {
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
|
||||
return focusedWs ? focusedWs.num : 1
|
||||
}
|
||||
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true)
|
||||
return focusedWs ? focusedWs.num : 1
|
||||
}
|
||||
|
||||
function getHyprlandWorkspaces() {
|
||||
const workspaces = Hyprland.workspaces?.values || []
|
||||
if (workspaces.length === 0) return [{id: 1}]
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return workspaces.slice().sort((a, b) => a.id - b.id)
|
||||
}
|
||||
|
||||
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
|
||||
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.id - b.id) : [{id: 1}]
|
||||
}
|
||||
|
||||
function getHyprlandActiveWorkspace() {
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return Hyprland.focusedWorkspace?.id || 1
|
||||
}
|
||||
|
||||
const monitor = Hyprland.monitors?.values?.find(m => m.name === root.screenName)
|
||||
return monitor?.activeWorkspace?.id || 1
|
||||
}
|
||||
|
||||
function getWorkspaceIcons(ws) {
|
||||
_desktopEntriesUpdateTrigger
|
||||
if (!SettingsData.showWorkspaceApps || !ws) {
|
||||
return []
|
||||
}
|
||||
|
||||
let targetWorkspaceId
|
||||
if (CompositorService.isNiri) {
|
||||
const wsNumber = typeof ws === "number" ? ws : -1
|
||||
if (wsNumber <= 0) {
|
||||
return []
|
||||
}
|
||||
const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.screenName)
|
||||
if (!workspace) {
|
||||
return []
|
||||
}
|
||||
targetWorkspaceId = workspace.id
|
||||
} else if (CompositorService.isHyprland) {
|
||||
targetWorkspaceId = ws.id !== undefined ? ws.id : ws
|
||||
} else if (CompositorService.isDwl) {
|
||||
if (typeof ws !== "object" || ws.tag === undefined) {
|
||||
return []
|
||||
}
|
||||
targetWorkspaceId = ws.tag
|
||||
} else if (CompositorService.isSway) {
|
||||
targetWorkspaceId = ws.num !== undefined ? ws.num : ws
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels
|
||||
|
||||
const byApp = {}
|
||||
let isActiveWs = false
|
||||
if (CompositorService.isNiri) {
|
||||
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active)
|
||||
} else if (CompositorService.isSway) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
|
||||
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false
|
||||
} else if (CompositorService.isDwl) {
|
||||
const output = DwlService.getOutputState(root.screenName)
|
||||
if (output && output.tags) {
|
||||
const tag = output.tags.find(t => t.tag === targetWorkspaceId)
|
||||
isActiveWs = tag ? (tag.state === 1) : false
|
||||
}
|
||||
} else {
|
||||
isActiveWs = targetWorkspaceId === root.currentWorkspace
|
||||
}
|
||||
|
||||
wins.forEach((w, i) => {
|
||||
if (!w) {
|
||||
return
|
||||
}
|
||||
|
||||
let winWs = null
|
||||
if (CompositorService.isNiri) {
|
||||
winWs = w.workspace_id
|
||||
} else if (CompositorService.isSway) {
|
||||
winWs = w.workspace?.num
|
||||
} else {
|
||||
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || [])
|
||||
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w)
|
||||
winWs = hyprToplevel?.workspace?.id
|
||||
}
|
||||
|
||||
|
||||
if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
|
||||
return
|
||||
}
|
||||
|
||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown")
|
||||
const key = isActiveWs ? `${keyBase}_${i}` : keyBase
|
||||
|
||||
if (!byApp[key]) {
|
||||
const moddedId = Paths.moddedAppId(keyBase)
|
||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
||||
const icon = isSteamApp ? "" : DesktopService.resolveIconPath(moddedId)
|
||||
byApp[key] = {
|
||||
"type": "icon",
|
||||
"icon": icon,
|
||||
"isSteamApp": isSteamApp,
|
||||
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
|
||||
"count": 1,
|
||||
"windowId": w.address || w.id,
|
||||
"fallbackText": w.appId || w.class || w.title || ""
|
||||
}
|
||||
} else {
|
||||
byApp[key].count++
|
||||
if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) {
|
||||
byApp[key].active = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Object.values(byApp)
|
||||
}
|
||||
|
||||
function padWorkspaces(list) {
|
||||
const padded = list.slice()
|
||||
let placeholder
|
||||
if (useExtWorkspace) {
|
||||
placeholder = {"id": "", "name": "", "active": false, "hidden": true}
|
||||
} else if (CompositorService.isHyprland) {
|
||||
placeholder = {"id": -1, "name": ""}
|
||||
} else if (CompositorService.isDwl) {
|
||||
placeholder = {"tag": -1}
|
||||
} else if (CompositorService.isSway) {
|
||||
placeholder = {"num": -1}
|
||||
} else {
|
||||
placeholder = -1
|
||||
}
|
||||
while (padded.length < 3) {
|
||||
padded.push(placeholder)
|
||||
}
|
||||
return padded
|
||||
}
|
||||
|
||||
function getNiriWorkspaces() {
|
||||
if (NiriService.allWorkspaces.length === 0) {
|
||||
return [1, 2]
|
||||
}
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return NiriService.getCurrentOutputWorkspaceNumbers()
|
||||
}
|
||||
|
||||
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1)
|
||||
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
|
||||
}
|
||||
|
||||
function getNiriActiveWorkspace() {
|
||||
if (NiriService.allWorkspaces.length === 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return NiriService.getCurrentWorkspaceNumber()
|
||||
}
|
||||
|
||||
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active)
|
||||
return activeWs ? activeWs.idx + 1 : 1
|
||||
}
|
||||
|
||||
function getDwlTags() {
|
||||
if (!DwlService.dwlAvailable) {
|
||||
return []
|
||||
}
|
||||
|
||||
const output = DwlService.getOutputState(root.screenName)
|
||||
if (!output || !output.tags || output.tags.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (SettingsData.dwlShowAllTags) {
|
||||
return output.tags.map(tag => ({"tag": tag.tag, "state": tag.state, "clients": tag.clients, "focused": tag.focused}))
|
||||
}
|
||||
|
||||
const visibleTagIndices = DwlService.getVisibleTags(root.screenName)
|
||||
return visibleTagIndices.map(tagIndex => {
|
||||
const tagData = output.tags.find(t => t.tag === tagIndex)
|
||||
return {
|
||||
"tag": tagIndex,
|
||||
"state": tagData?.state ?? 0,
|
||||
"clients": tagData?.clients ?? 0,
|
||||
"focused": tagData?.focused ?? false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getDwlActiveTags() {
|
||||
if (!DwlService.dwlAvailable) {
|
||||
return []
|
||||
}
|
||||
|
||||
const activeTags = DwlService.getActiveTags(root.screenName)
|
||||
return activeTags
|
||||
}
|
||||
|
||||
function getExtWorkspaceWorkspaces() {
|
||||
const groups = ExtWorkspaceService.groups
|
||||
if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) {
|
||||
return [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName))
|
||||
if (!group || !group.workspaces) {
|
||||
return [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
const visible = group.workspaces.filter(ws => !ws.hidden).sort((a, b) => {
|
||||
const coordsA = a.coordinates || [0, 0]
|
||||
const coordsB = b.coordinates || [0, 0]
|
||||
if (coordsA[0] !== coordsB[0]) return coordsA[0] - coordsB[0]
|
||||
return coordsA[1] - coordsB[1]
|
||||
}).map(ws => ({
|
||||
id: ws.id,
|
||||
name: ws.name,
|
||||
coordinates: ws.coordinates,
|
||||
state: ws.state,
|
||||
active: ws.active,
|
||||
urgent: ws.urgent,
|
||||
hidden: ws.hidden,
|
||||
groupID: group.id
|
||||
}))
|
||||
|
||||
return visible.length > 0 ? visible : [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
function getExtWorkspaceActiveWorkspace() {
|
||||
if (!ExtWorkspaceService.extWorkspaceAvailable) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName)
|
||||
return activeWs ? (activeWs.id || activeWs.name || "1") : "1"
|
||||
}
|
||||
|
||||
readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
||||
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
|
||||
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
|
||||
|
||||
function getRealWorkspaces() {
|
||||
return root.workspaceList.filter(ws => {
|
||||
if (useExtWorkspace) return ws && ws.id !== "" && !ws.hidden
|
||||
if (CompositorService.isHyprland) return ws && ws.id !== -1
|
||||
if (CompositorService.isDwl) return ws && ws.tag !== -1
|
||||
if (CompositorService.isSway) return ws && ws.num !== -1
|
||||
return ws !== -1
|
||||
})
|
||||
}
|
||||
|
||||
function switchWorkspace(direction) {
|
||||
if (useExtWorkspace) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextWorkspace = realWorkspaces[nextIndex]
|
||||
ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || "")
|
||||
} else if (CompositorService.isNiri) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1)
|
||||
} else if (CompositorService.isHyprland) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws.id === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`)
|
||||
} else if (CompositorService.isDwl) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag)
|
||||
} else if (CompositorService.isSway) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){}
|
||||
}
|
||||
}
|
||||
|
||||
width: isVertical ? barThickness : visualWidth
|
||||
height: isVertical ? visualHeight : barThickness
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || useExtWorkspace
|
||||
|
||||
Rectangle {
|
||||
id: visualBackground
|
||||
width: root.visualWidth
|
||||
height: root.visualHeight
|
||||
anchors.centerIn: parent
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.dankBarNoBackground)
|
||||
return "transparent"
|
||||
const baseColor = Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.toggleOverview()
|
||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: workspaceRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.workspaceList
|
||||
}
|
||||
|
||||
Item {
|
||||
id: delegateRoot
|
||||
|
||||
property bool isActive: {
|
||||
if (root.useExtWorkspace) return (modelData?.id || modelData?.name) === root.currentWorkspace
|
||||
if (CompositorService.isHyprland) return !!(modelData && modelData.id === root.currentWorkspace)
|
||||
if (CompositorService.isDwl) return !!(modelData && root.dwlActiveTags.includes(modelData.tag))
|
||||
if (CompositorService.isSway) return !!(modelData && modelData.num === root.currentWorkspace)
|
||||
return modelData === root.currentWorkspace
|
||||
}
|
||||
property bool isPlaceholder: {
|
||||
if (root.useExtWorkspace) return !!(modelData && modelData.hidden)
|
||||
if (CompositorService.isHyprland) return !!(modelData && modelData.id === -1)
|
||||
if (CompositorService.isDwl) return !!(modelData && modelData.tag === -1)
|
||||
if (CompositorService.isSway) return !!(modelData && modelData.num === -1)
|
||||
return modelData === -1
|
||||
}
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
|
||||
property var loadedWorkspaceData: null
|
||||
property bool loadedIsUrgent: false
|
||||
property bool isUrgent: {
|
||||
if (root.useExtWorkspace) return modelData?.urgent ?? false
|
||||
if (CompositorService.isHyprland) return modelData?.urgent ?? false
|
||||
if (CompositorService.isNiri) return loadedIsUrgent
|
||||
if (CompositorService.isDwl) return modelData?.state === 2
|
||||
if (CompositorService.isSway) return loadedIsUrgent
|
||||
return false
|
||||
}
|
||||
property var loadedIconData: null
|
||||
property bool loadedHasIcon: false
|
||||
property var loadedIcons: []
|
||||
|
||||
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
|
||||
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
|
||||
|
||||
readonly property real iconsExtraWidth: {
|
||||
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
|
||||
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
readonly property real iconsExtraHeight: {
|
||||
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
|
||||
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
readonly property real visualWidth: baseWidth + iconsExtraWidth
|
||||
readonly property real visualHeight: baseHeight + iconsExtraHeight
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: !isPlaceholder
|
||||
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
enabled: !isPlaceholder
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (isPlaceholder) return
|
||||
|
||||
const isRightClick = mouse.button === Qt.RightButton
|
||||
|
||||
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
|
||||
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "")
|
||||
} else if (CompositorService.isNiri) {
|
||||
if (isRightClick) {
|
||||
NiriService.toggleOverview()
|
||||
} else {
|
||||
NiriService.switchToWorkspace(modelData - 1)
|
||||
}
|
||||
} else if (CompositorService.isHyprland && modelData?.id) {
|
||||
if (isRightClick && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
||||
} else {
|
||||
Hyprland.dispatch(`workspace ${modelData.id}`)
|
||||
}
|
||||
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
|
||||
console.log("DWL click - tag:", modelData.tag, "rightClick:", isRightClick)
|
||||
if (isRightClick) {
|
||||
console.log("Calling toggleTag")
|
||||
DwlService.toggleTag(root.screenName, modelData.tag)
|
||||
} else {
|
||||
console.log("Calling switchToTag")
|
||||
DwlService.switchToTag(root.screenName, modelData.tag)
|
||||
}
|
||||
} else if (CompositorService.isSway && modelData?.num) {
|
||||
try { I3.dispatch(`workspace number ${modelData.num}`) } catch(_){}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dataUpdateTimer
|
||||
interval: 50
|
||||
onTriggered: {
|
||||
if (isPlaceholder) {
|
||||
delegateRoot.loadedWorkspaceData = null
|
||||
delegateRoot.loadedIconData = null
|
||||
delegateRoot.loadedHasIcon = false
|
||||
delegateRoot.loadedIcons = []
|
||||
delegateRoot.loadedIsUrgent = false
|
||||
return
|
||||
}
|
||||
|
||||
var wsData = null;
|
||||
if (root.useExtWorkspace) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isNiri) {
|
||||
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isDwl) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isSway) {
|
||||
wsData = modelData;
|
||||
}
|
||||
delegateRoot.loadedWorkspaceData = wsData;
|
||||
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
|
||||
|
||||
var icData = null;
|
||||
if (wsData?.name) {
|
||||
icData = SettingsData.getWorkspaceNameIcon(wsData.name);
|
||||
}
|
||||
delegateRoot.loadedIconData = icData;
|
||||
delegateRoot.loadedHasIcon = icData !== null;
|
||||
|
||||
if (SettingsData.showWorkspaceApps) {
|
||||
if (CompositorService.isDwl || CompositorService.isSway) {
|
||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
|
||||
} else {
|
||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
|
||||
}
|
||||
} else {
|
||||
delegateRoot.loadedIcons = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllData() {
|
||||
dataUpdateTimer.restart()
|
||||
}
|
||||
|
||||
width: root.isVertical ? root.barThickness : visualWidth
|
||||
height: root.isVertical ? visualHeight : root.barThickness
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: delegateRoot.visualWidth
|
||||
height: delegateRoot.visualHeight
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
|
||||
|
||||
border.width: isUrgent && !isActive ? 2 : 0
|
||||
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0)
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appIconsLoader
|
||||
anchors.fill: parent
|
||||
active: SettingsData.showWorkspaceApps
|
||||
sourceComponent: Item {
|
||||
Loader {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: root.isVertical ? columnLayout : rowLayout
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowLayout
|
||||
Row {
|
||||
spacing: 4
|
||||
visible: loadedIcons.length > 0
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
}
|
||||
delegate: Item {
|
||||
width: 18
|
||||
height: 18
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
property var windowId: modelData.windowId
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: !modelData.isSteamApp
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: 18
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: modelData.isSteamApp
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: appMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: isActive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!appIcon.windowId) return
|
||||
if (CompositorService.isHyprland) {
|
||||
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.focusWindow(appIcon.windowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: modelData.count > 1 && !isActive
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: "black"
|
||||
border.color: "white"
|
||||
border.width: 1
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
z: 2
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.count
|
||||
font.pixelSize: 8
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: columnLayout
|
||||
Column {
|
||||
spacing: 4
|
||||
visible: loadedIcons.length > 0
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
}
|
||||
delegate: Item {
|
||||
width: 18
|
||||
height: 18
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
property var windowId: modelData.windowId
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: !modelData.isSteamApp
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: 18
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: modelData.isSteamApp
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: appMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: isActive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!appIcon.windowId) return
|
||||
if (CompositorService.isHyprland) {
|
||||
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.focusWindow(appIcon.windowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: modelData.count > 1 && !isActive
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: "black"
|
||||
border.color: "white"
|
||||
border.width: 1
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
z: 2
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.count
|
||||
font.pixelSize: 8
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loader for Custom Name Icon
|
||||
Loader {
|
||||
id: customIconLoader
|
||||
anchors.fill: parent
|
||||
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "icon" && !SettingsData.showWorkspaceApps
|
||||
sourceComponent: Item {
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: loadedIconData ? loadedIconData.value : "" // NULL CHECK
|
||||
size: Theme.fontSizeSmall
|
||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
|
||||
weight: isActive && !isPlaceholder ? 500 : 400
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loader for Custom Name Text
|
||||
Loader {
|
||||
id: customTextLoader
|
||||
anchors.fill: parent
|
||||
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "text" && !SettingsData.showWorkspaceApps
|
||||
sourceComponent: Item {
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: loadedIconData ? loadedIconData.value : "" // NULL CHECK
|
||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loader for Workspace Index
|
||||
Loader {
|
||||
id: indexLoader
|
||||
anchors.fill: parent
|
||||
active: !isPlaceholder && SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
|
||||
sourceComponent: Item {
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
let isPlaceholder
|
||||
if (root.useExtWorkspace) {
|
||||
isPlaceholder = modelData?.hidden === true
|
||||
} else if (CompositorService.isHyprland) {
|
||||
isPlaceholder = modelData?.id === -1
|
||||
} else if (CompositorService.isDwl) {
|
||||
isPlaceholder = modelData?.tag === -1
|
||||
} else if (CompositorService.isSway) {
|
||||
isPlaceholder = modelData?.num === -1
|
||||
} else {
|
||||
isPlaceholder = modelData === -1
|
||||
}
|
||||
|
||||
if (isPlaceholder) return index + 1
|
||||
|
||||
if (root.useExtWorkspace) return modelData?.name || modelData?.id || ""
|
||||
if (CompositorService.isHyprland) return modelData?.id || ""
|
||||
if (CompositorService.isDwl) return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""
|
||||
if (CompositorService.isSway) return modelData?.num || ""
|
||||
return modelData - 1
|
||||
}
|
||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: updateAllData()
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onSortedToplevelsChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: NiriService
|
||||
enabled: CompositorService.isNiri
|
||||
function onAllWorkspacesChanged() { delegateRoot.updateAllData() }
|
||||
function onWindowUrgentChanged() { delegateRoot.updateAllData() }
|
||||
function onWindowsChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() }
|
||||
function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: DwlService
|
||||
enabled: CompositorService.isDwl
|
||||
function onStateChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: Hyprland.workspaces
|
||||
enabled: CompositorService.isHyprland
|
||||
function onValuesChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: I3.workspaces
|
||||
enabled: CompositorService.isSway
|
||||
function onValuesChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: ExtWorkspaceService
|
||||
enabled: root.useExtWorkspace
|
||||
function onStateChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
|
||||
DMSService.addSubscription("extworkspace")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.DankDash
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:dash"
|
||||
|
||||
property bool dashVisible: false
|
||||
property var triggerScreen: null
|
||||
property int currentTabIndex: 0
|
||||
|
||||
keyboardFocusMode: WlrKeyboardFocus.Exclusive
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerSection = section
|
||||
triggerScreen = screen
|
||||
triggerY = y
|
||||
|
||||
if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)) {
|
||||
const screenWidth = screen ? screen.width : Screen.width
|
||||
triggerX = (screenWidth - popupWidth) / 2
|
||||
triggerWidth = popupWidth
|
||||
} else if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)) {
|
||||
const screenHeight = screen ? screen.height : Screen.height
|
||||
triggerX = (screenHeight - popupHeight) / 2
|
||||
triggerWidth = popupHeight
|
||||
} else {
|
||||
triggerX = x
|
||||
triggerWidth = width
|
||||
}
|
||||
}
|
||||
|
||||
popupWidth: 700
|
||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||
triggerX: Screen.width - 620 - Theme.spacingL
|
||||
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
|
||||
triggerWidth: 80
|
||||
shouldBeVisible: dashVisible
|
||||
visible: shouldBeVisible
|
||||
|
||||
property bool __focusArmed: false
|
||||
property bool __contentReady: false
|
||||
|
||||
function __tryFocusOnce() {
|
||||
if (!__focusArmed)
|
||||
return
|
||||
const win = root.window
|
||||
if (!win || !win.visible)
|
||||
return
|
||||
if (!contentLoader.item)
|
||||
return
|
||||
|
||||
if (win.requestActivate)
|
||||
win.requestActivate()
|
||||
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
|
||||
|
||||
if (contentLoader.item.activeFocus)
|
||||
__focusArmed = false
|
||||
}
|
||||
|
||||
onDashVisibleChanged: {
|
||||
if (dashVisible) {
|
||||
__focusArmed = true
|
||||
__contentReady = !!contentLoader.item
|
||||
open()
|
||||
__tryFocusOnce()
|
||||
} else {
|
||||
__focusArmed = false
|
||||
__contentReady = false
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentLoader
|
||||
function onLoaded() {
|
||||
__contentReady = true
|
||||
if (__focusArmed)
|
||||
__tryFocusOnce()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.window ? root.window : null
|
||||
enabled: !!root.window
|
||||
function onVisibleChanged() {
|
||||
if (__focusArmed)
|
||||
__tryFocusOnce()
|
||||
}
|
||||
}
|
||||
|
||||
onBackgroundClicked: {
|
||||
dashVisible = false
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: mainContainer
|
||||
|
||||
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible) {
|
||||
mainContainer.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
Qt.callLater(function () {
|
||||
mainContainer.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.dashVisible = false
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
|
||||
let nextIndex = root.currentTabIndex + 1
|
||||
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
|
||||
nextIndex++
|
||||
}
|
||||
if (nextIndex >= tabBar.model.length) {
|
||||
nextIndex = 0
|
||||
}
|
||||
root.currentTabIndex = nextIndex
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
|
||||
let prevIndex = root.currentTabIndex - 1
|
||||
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
|
||||
prevIndex--
|
||||
}
|
||||
if (prevIndex < 0) {
|
||||
prevIndex = tabBar.model.length - 1
|
||||
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
|
||||
prevIndex--
|
||||
}
|
||||
}
|
||||
if (prevIndex >= 0) {
|
||||
root.currentTabIndex = prevIndex
|
||||
}
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
|
||||
if (wallpaperTab.handleKeyEvent(event)) {
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root.shouldBeVisible
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.08
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.02
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankTabBar {
|
||||
id: tabBar
|
||||
|
||||
width: parent.width
|
||||
height: 48
|
||||
currentIndex: root.currentTabIndex
|
||||
spacing: Theme.spacingS
|
||||
equalWidthTabs: true
|
||||
enableArrowNavigation: false
|
||||
focus: false
|
||||
activeFocusOnTab: false
|
||||
nextFocusTarget: {
|
||||
const item = pages.currentItem
|
||||
if (!item)
|
||||
return null
|
||||
if (item.focusTarget)
|
||||
return item.focusTarget
|
||||
return item
|
||||
}
|
||||
|
||||
model: {
|
||||
let tabs = [{
|
||||
"icon": "dashboard",
|
||||
"text": I18n.tr("Overview")
|
||||
}, {
|
||||
"icon": "music_note",
|
||||
"text": I18n.tr("Media")
|
||||
}, {
|
||||
"icon": "wallpaper",
|
||||
"text": I18n.tr("Wallpapers")
|
||||
}]
|
||||
|
||||
if (SettingsData.weatherEnabled) {
|
||||
tabs.push({
|
||||
"icon": "wb_sunny",
|
||||
"text": I18n.tr("Weather")
|
||||
})
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
"icon": "settings",
|
||||
"text": I18n.tr("Settings"),
|
||||
"isAction": true
|
||||
})
|
||||
return tabs
|
||||
}
|
||||
|
||||
onTabClicked: function (index) {
|
||||
root.currentTabIndex = index
|
||||
}
|
||||
|
||||
onActionTriggered: function (index) {
|
||||
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
|
||||
if (index === settingsIndex) {
|
||||
dashVisible = false
|
||||
settingsModal.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.spacingXS
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: pages
|
||||
width: parent.width
|
||||
implicitHeight: {
|
||||
if (currentIndex === 0)
|
||||
return overviewTab.implicitHeight
|
||||
if (currentIndex === 1)
|
||||
return mediaTab.implicitHeight
|
||||
if (currentIndex === 2)
|
||||
return wallpaperTab.implicitHeight
|
||||
if (SettingsData.weatherEnabled && currentIndex === 3)
|
||||
return weatherTab.implicitHeight
|
||||
return overviewTab.implicitHeight
|
||||
}
|
||||
currentIndex: root.currentTabIndex
|
||||
|
||||
OverviewTab {
|
||||
id: overviewTab
|
||||
|
||||
onCloseDash: {
|
||||
root.dashVisible = false
|
||||
}
|
||||
|
||||
onSwitchToWeatherTab: {
|
||||
if (SettingsData.weatherEnabled) {
|
||||
tabBar.currentIndex = 3
|
||||
tabBar.tabClicked(3)
|
||||
}
|
||||
}
|
||||
|
||||
onSwitchToMediaTab: {
|
||||
tabBar.currentIndex = 1
|
||||
tabBar.tabClicked(1)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayerTab {
|
||||
id: mediaTab
|
||||
}
|
||||
|
||||
WallpaperTab {
|
||||
id: wallpaperTab
|
||||
active: root.currentTabIndex === 2
|
||||
tabBarItem: tabBar
|
||||
keyForwardTarget: mainContainer
|
||||
targetScreen: root.triggerScreen
|
||||
}
|
||||
|
||||
WeatherTab {
|
||||
id: weatherTab
|
||||
visible: SettingsData.weatherEnabled && root.currentTabIndex === 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,642 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: 700
|
||||
implicitHeight: 410
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
|
||||
|
||||
DankIcon {
|
||||
name: "cloud_off"
|
||||
size: Theme.iconSize * 2
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No Weather Data Available")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 70
|
||||
|
||||
DankIcon {
|
||||
id: refreshButton
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 4
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
property bool isRefreshing: false
|
||||
enabled: !isRefreshing
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
||||
onClicked: {
|
||||
refreshButton.isRefreshing = true
|
||||
WeatherService.forceRefresh()
|
||||
refreshTimer.restart()
|
||||
}
|
||||
enabled: parent.enabled
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 2000
|
||||
onTriggered: refreshButton.isRefreshing = false
|
||||
}
|
||||
|
||||
NumberAnimation on rotation {
|
||||
running: refreshButton.isRefreshing
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2
|
||||
height: 70
|
||||
|
||||
DankIcon {
|
||||
id: weatherIcon
|
||||
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
||||
size: Theme.iconSize * 1.5
|
||||
color: Theme.primary
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.8
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.2)
|
||||
shadowOpacity: 0.2
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: tempColumn
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: weatherIcon.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Item {
|
||||
width: tempText.width + unitText.width + Theme.spacingXS
|
||||
height: tempText.height
|
||||
|
||||
StyledText {
|
||||
id: tempText
|
||||
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
|
||||
font.pixelSize: Theme.fontSizeLarge + 4
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Light
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: unitText
|
||||
text: SettingsData.useFahrenheit ? "F" : "C"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.left: tempText.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (WeatherService.weather.available) {
|
||||
SettingsData.set("temperatureUnit", !SettingsData.useFahrenheit)
|
||||
}
|
||||
}
|
||||
enabled: WeatherService.weather.available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.city || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: sunriseColumn
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: tempColumn.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: WeatherService.weather.sunrise && WeatherService.weather.sunset
|
||||
|
||||
Item {
|
||||
width: sunriseIcon.width + sunriseText.width + Theme.spacingXS
|
||||
height: sunriseIcon.height
|
||||
|
||||
DankIcon {
|
||||
id: sunriseIcon
|
||||
name: "wb_twilight"
|
||||
size: Theme.iconSize - 6
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: sunriseText
|
||||
text: WeatherService.weather.sunrise || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: sunriseIcon.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: sunsetIcon.width + sunsetText.width + Theme.spacingXS
|
||||
height: sunsetIcon.height
|
||||
|
||||
DankIcon {
|
||||
id: sunsetIcon
|
||||
name: "bedtime"
|
||||
size: Theme.iconSize - 6
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: sunsetText
|
||||
text: WeatherService.weather.sunset || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: sunsetIcon.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
width: parent.width
|
||||
height: 95
|
||||
columns: 6
|
||||
columnSpacing: Theme.spacingS
|
||||
rowSpacing: 0
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "device_thermostat"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Feels Like")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "humidity_low"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Humidity")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "air"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Wind")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.wind || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "speed"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Pressure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.pressure ? WeatherService.weather.pressure + " hPa" : "--"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "rainy"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Rain Chance")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "wb_sunny"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Visibility")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Good")
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
height: parent.height - 70 - 95 - Theme.spacingM * 3 - 2
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("7-Day Forecast")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: parent.height - Theme.fontSizeMedium - Theme.spacingS - Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: 7
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingXS * 6) / 7
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
property var dayDate: {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + index)
|
||||
return date
|
||||
}
|
||||
property bool isToday: index === 0
|
||||
property var forecastData: {
|
||||
if (WeatherService.weather.forecast && WeatherService.weather.forecast.length > index) {
|
||||
return WeatherService.weather.forecast[index]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
|
||||
border.width: isToday ? 1 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: Qt.locale().dayName(dayDate.getDay(), Locale.ShortFormat)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: isToday ? Theme.primary : Theme.surfaceText
|
||||
font.weight: isToday ? Font.Medium : Font.Normal
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: forecastData ? WeatherService.getWeatherIcon(forecastData.wCode || 0) : "cloud"
|
||||
size: Theme.iconSize
|
||||
color: isToday ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: forecastData ? (SettingsData.useFahrenheit ? (forecastData.tempMaxF || forecastData.tempMax) : (forecastData.tempMax || 0)) + "°/" + (SettingsData.useFahrenheit ? (forecastData.tempMinF || forecastData.tempMin) : (forecastData.tempMin || 0)) + "°" : "--/--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: isToday ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: forecastData && forecastData.sunrise && forecastData.sunset
|
||||
|
||||
Row {
|
||||
spacing: 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "wb_twilight"
|
||||
size: 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: forecastData ? forecastData.sunrise : ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: forecastData ? forecastData.sunset : ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Variants {
|
||||
id: dockVariants
|
||||
model: SettingsData.getFilteredScreens("dock")
|
||||
|
||||
property var contextMenu
|
||||
|
||||
delegate: PanelWindow {
|
||||
id: dock
|
||||
|
||||
WlrLayershell.namespace: "dms:dock"
|
||||
|
||||
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
|
||||
anchors {
|
||||
top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
|
||||
bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
|
||||
left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
|
||||
right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
}
|
||||
|
||||
property var modelData: item
|
||||
property bool autoHide: SettingsData.dockAutoHide
|
||||
property real backgroundTransparency: SettingsData.dockTransparency
|
||||
property bool groupByApp: SettingsData.dockGroupByApp
|
||||
|
||||
readonly property real widgetHeight: SettingsData.dockIconSize
|
||||
readonly property real effectiveBarHeight: widgetHeight + SettingsData.dockSpacing * 2 + 10
|
||||
readonly property real barSpacing: {
|
||||
const barIsHorizontal = (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)
|
||||
const barIsVertical = (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)
|
||||
const samePosition = (SettingsData.dockPosition === SettingsData.dankBarPosition)
|
||||
const dockIsHorizontal = !isVertical
|
||||
const dockIsVertical = isVertical
|
||||
|
||||
if (!SettingsData.dankBarVisible) return 0
|
||||
if (dockIsHorizontal && barIsHorizontal && samePosition) {
|
||||
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
|
||||
}
|
||||
if (dockIsVertical && barIsVertical && samePosition) {
|
||||
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
readonly property real dockMargin: SettingsData.dockSpacing
|
||||
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
|
||||
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
|
||||
function px(v) { return Math.round(v * _dpr) / _dpr }
|
||||
|
||||
|
||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
|
||||
property bool revealSticky: false
|
||||
|
||||
Timer {
|
||||
id: revealHold
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: dock.revealSticky = false
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
|
||||
return true
|
||||
}
|
||||
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
|
||||
}
|
||||
|
||||
onContextMenuOpenChanged: {
|
||||
if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) {
|
||||
revealSticky = true
|
||||
revealHold.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDockTransparencyChanged() {
|
||||
dock.backgroundTransparency = SettingsData.dockTransparency
|
||||
}
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
visible: {
|
||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
||||
return SettingsData.dockOpenOnOverview
|
||||
}
|
||||
return SettingsData.showDock
|
||||
}
|
||||
color: "transparent"
|
||||
|
||||
|
||||
exclusiveZone: {
|
||||
if (!SettingsData.showDock || autoHide) return -1
|
||||
if (barSpacing > 0) return -1
|
||||
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin)
|
||||
}
|
||||
|
||||
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
|
||||
|
||||
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
|
||||
Item {
|
||||
id: maskItem
|
||||
parent: dock.contentItem
|
||||
visible: false
|
||||
x: {
|
||||
const baseX = dockCore.x + dockMouseArea.x
|
||||
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return baseX - animationHeadroom
|
||||
}
|
||||
return baseX
|
||||
}
|
||||
y: {
|
||||
const baseY = dockCore.y + dockMouseArea.y
|
||||
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return baseY - animationHeadroom
|
||||
}
|
||||
return baseY
|
||||
}
|
||||
width: dockMouseArea.width + (isVertical ? animationHeadroom : 0)
|
||||
height: dockMouseArea.height + (!isVertical ? animationHeadroom : 0)
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: maskItem
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
DankTooltip {
|
||||
id: dockTooltip
|
||||
targetScreen: dock.screen
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: tooltipRevealDelay
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: dock.showTooltipForHoveredButton()
|
||||
}
|
||||
|
||||
function showTooltipForHoveredButton() {
|
||||
dockTooltip.hide()
|
||||
if (dock.hoveredButton && dock.reveal && !slideXAnimation.running && !slideYAnimation.running) {
|
||||
const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0)
|
||||
const tooltipText = dock.hoveredButton.tooltipText || ""
|
||||
if (tooltipText) {
|
||||
const screenX = dock.screen ? (dock.screen.x || 0) : 0
|
||||
const screenY = dock.screen ? (dock.screen.y || 0) : 0
|
||||
const screenHeight = dock.screen ? dock.screen.height : 0
|
||||
if (!dock.isVertical) {
|
||||
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2
|
||||
const screenRelativeY = isBottom
|
||||
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - SettingsData.dockMargin - 35)
|
||||
: (buttonGlobalPos.y - screenY + dock.hoveredButton.height + Theme.spacingS)
|
||||
dockTooltip.show(tooltipText,
|
||||
globalX,
|
||||
screenRelativeY,
|
||||
dock.screen,
|
||||
false, false)
|
||||
} else {
|
||||
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left
|
||||
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dock
|
||||
function onRevealChanged() {
|
||||
if (!dock.reveal) {
|
||||
tooltipRevealDelay.stop()
|
||||
dockTooltip.hide()
|
||||
} else {
|
||||
tooltipRevealDelay.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function onHoveredButtonChanged() {
|
||||
dock.showTooltipForHoveredButton()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockCore
|
||||
anchors.fill: parent
|
||||
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
|
||||
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
|
||||
|
||||
Connections {
|
||||
target: dockMouseArea
|
||||
function onContainsMouseChanged() {
|
||||
if (dockMouseArea.containsMouse) {
|
||||
dock.revealSticky = true
|
||||
revealHold.stop()
|
||||
} else {
|
||||
if (dock.autoHide && !dock.contextMenuOpen) {
|
||||
revealHold.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dockMouseArea
|
||||
property real currentScreen: modelData ? modelData : dock.screen
|
||||
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
|
||||
property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080
|
||||
property real maxDockWidth: screenWidth * 0.98
|
||||
property real maxDockHeight: screenHeight * 0.98
|
||||
|
||||
height: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitHeight + 4, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
|
||||
} else {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
|
||||
}
|
||||
}
|
||||
width: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
|
||||
} else {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitWidth + 4, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
|
||||
}
|
||||
}
|
||||
anchors {
|
||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
|
||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockContainer
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
|
||||
transform: Translate {
|
||||
id: dockSlide
|
||||
x: {
|
||||
if (!dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return hideDistance
|
||||
} else {
|
||||
return -hideDistance
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return hideDistance
|
||||
} else {
|
||||
return -hideDistance
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
id: slideXAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
id: slideYAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockBackground
|
||||
objectName: "dockBackground"
|
||||
anchors {
|
||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
|
||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
|
||||
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
|
||||
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
layer.enabled: true
|
||||
clip: false
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
|
||||
overlayColor: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
}
|
||||
}
|
||||
|
||||
DockApps {
|
||||
id: dockApps
|
||||
|
||||
anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
|
||||
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
|
||||
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
|
||||
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
|
||||
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
|
||||
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
|
||||
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
|
||||
contextMenu: dockVariants.contextMenu
|
||||
groupByApp: dock.groupByApp
|
||||
isVertical: dock.isVertical
|
||||
dockScreen: dock.screen
|
||||
iconSize: dock.widgetHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var contextMenu: null
|
||||
property bool requestDockShow: false
|
||||
property int pinnedAppCount: 0
|
||||
property bool groupByApp: false
|
||||
property bool isVertical: false
|
||||
property var dockScreen: null
|
||||
property real iconSize: 40
|
||||
|
||||
clip: false
|
||||
implicitWidth: isVertical ? appLayout.height : appLayout.width
|
||||
implicitHeight: isVertical ? appLayout.width : appLayout.height
|
||||
|
||||
function movePinnedApp(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentPinned = [...(SessionData.pinnedApps || [])]
|
||||
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const movedApp = currentPinned.splice(fromIndex, 1)[0]
|
||||
currentPinned.splice(toIndex, 0, movedApp)
|
||||
|
||||
SessionData.setPinnedApps(currentPinned)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: appLayout
|
||||
width: layoutFlow.width
|
||||
height: layoutFlow.height
|
||||
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
|
||||
anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
|
||||
anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
|
||||
anchors.top: root.isVertical ? undefined : parent.top
|
||||
|
||||
Flow {
|
||||
id: layoutFlow
|
||||
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
property var dockItems: []
|
||||
|
||||
model: ScriptModel {
|
||||
values: repeater.dockItems
|
||||
objectProp: "uniqueKey"
|
||||
}
|
||||
|
||||
Component.onCompleted: updateModel()
|
||||
|
||||
function updateModel() {
|
||||
const items = []
|
||||
const pinnedApps = [...(SessionData.pinnedApps || [])]
|
||||
const sortedToplevels = CompositorService.sortedToplevels
|
||||
|
||||
if (root.groupByApp) {
|
||||
const appGroups = new Map()
|
||||
|
||||
pinnedApps.forEach(appId => {
|
||||
appGroups.set(appId, {
|
||||
appId: appId,
|
||||
isPinned: true,
|
||||
windows: []
|
||||
})
|
||||
})
|
||||
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
const appId = toplevel.appId || "unknown"
|
||||
if (!appGroups.has(appId)) {
|
||||
appGroups.set(appId, {
|
||||
appId: appId,
|
||||
isPinned: false,
|
||||
windows: []
|
||||
})
|
||||
}
|
||||
|
||||
appGroups.get(appId).windows.push({
|
||||
toplevel: toplevel,
|
||||
index: index
|
||||
})
|
||||
})
|
||||
|
||||
const pinnedGroups = []
|
||||
const unpinnedGroups = []
|
||||
|
||||
Array.from(appGroups.entries()).forEach(([appId, group]) => {
|
||||
const firstWindow = group.windows.length > 0 ? group.windows[0] : null
|
||||
|
||||
const item = {
|
||||
uniqueKey: "grouped_" + appId,
|
||||
type: "grouped",
|
||||
appId: appId,
|
||||
toplevel: firstWindow ? firstWindow.toplevel : null,
|
||||
isPinned: group.isPinned,
|
||||
isRunning: group.windows.length > 0,
|
||||
windowCount: group.windows.length,
|
||||
allWindows: group.windows
|
||||
}
|
||||
|
||||
if (group.isPinned) {
|
||||
pinnedGroups.push(item)
|
||||
} else {
|
||||
unpinnedGroups.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
pinnedGroups.forEach(item => items.push(item))
|
||||
|
||||
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
||||
items.push({
|
||||
uniqueKey: "separator_grouped",
|
||||
type: "separator",
|
||||
appId: "__SEPARATOR__",
|
||||
toplevel: null,
|
||||
isPinned: false,
|
||||
isRunning: false
|
||||
})
|
||||
}
|
||||
|
||||
unpinnedGroups.forEach(item => items.push(item))
|
||||
root.pinnedAppCount = pinnedGroups.length
|
||||
} else {
|
||||
pinnedApps.forEach(appId => {
|
||||
items.push({
|
||||
uniqueKey: "pinned_" + appId,
|
||||
type: "pinned",
|
||||
appId: appId,
|
||||
toplevel: null,
|
||||
isPinned: true,
|
||||
isRunning: false
|
||||
})
|
||||
})
|
||||
|
||||
root.pinnedAppCount = pinnedApps.length
|
||||
|
||||
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
|
||||
items.push({
|
||||
uniqueKey: "separator_ungrouped",
|
||||
type: "separator",
|
||||
appId: "__SEPARATOR__",
|
||||
toplevel: null,
|
||||
isPinned: false,
|
||||
isRunning: false
|
||||
})
|
||||
}
|
||||
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
let uniqueKey = "window_" + index
|
||||
if (CompositorService.isHyprland && Hyprland.toplevels) {
|
||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
|
||||
for (let i = 0; i < hyprlandToplevels.length; i++) {
|
||||
if (hyprlandToplevels[i].wayland === toplevel) {
|
||||
uniqueKey = "window_" + hyprlandToplevels[i].address
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
uniqueKey: uniqueKey,
|
||||
type: "window",
|
||||
appId: toplevel.appId,
|
||||
toplevel: toplevel,
|
||||
isPinned: false,
|
||||
isRunning: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
dockItems = items
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
property alias dockButton: button
|
||||
property var itemData: modelData
|
||||
clip: false
|
||||
|
||||
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
|
||||
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
|
||||
|
||||
Rectangle {
|
||||
visible: itemData.type === "separator"
|
||||
width: root.isVertical ? root.iconSize * 0.5 : 2
|
||||
height: root.isVertical ? 2 : root.iconSize * 0.5
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
radius: 1
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
DockAppButton {
|
||||
id: button
|
||||
visible: itemData.type !== "separator"
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: delegateItem.width
|
||||
height: delegateItem.height
|
||||
actualIconSize: root.iconSize
|
||||
|
||||
appData: itemData
|
||||
contextMenu: root.contextMenu
|
||||
dockApps: root
|
||||
index: model.index
|
||||
parentDockScreen: root.dockScreen
|
||||
|
||||
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
|
||||
windowTitle: {
|
||||
const title = itemData?.toplevel?.title || "(Unnamed)"
|
||||
return title.length > 50 ? title.substring(0, 47) + "..." : title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onToplevelsChanged() {
|
||||
repeater.updateModel()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onPinnedAppsChanged() {
|
||||
repeater.updateModel()
|
||||
}
|
||||
}
|
||||
|
||||
onGroupByAppChanged: {
|
||||
repeater.updateModel()
|
||||
}
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool isVisible: false
|
||||
property bool showLogout: true
|
||||
property int selectedIndex: 0
|
||||
property int optionCount: {
|
||||
let count = 0
|
||||
if (showLogout) count++
|
||||
count++
|
||||
if (SessionService.hibernateSupported) count++
|
||||
count += 2
|
||||
return count
|
||||
}
|
||||
|
||||
signal closed()
|
||||
|
||||
function show() {
|
||||
isVisible = true
|
||||
selectedIndex = 0
|
||||
Qt.callLater(() => {
|
||||
if (powerMenuFocusScope && powerMenuFocusScope.forceActiveFocus) {
|
||||
powerMenuFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
isVisible = false
|
||||
closed()
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
visible: isVisible
|
||||
z: 1000
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.hide()
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: powerMenuFocusScope
|
||||
anchors.fill: parent
|
||||
focus: root.isVisible
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
Qt.callLater(() => forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
root.hide()
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Up:
|
||||
case Qt.Key_Backtab:
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Down:
|
||||
case Qt.Key_Tab:
|
||||
selectedIndex = (selectedIndex + 1) % optionCount
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
const actions = []
|
||||
if (showLogout) actions.push("logout")
|
||||
actions.push("suspend")
|
||||
if (SessionService.hibernateSupported) actions.push("hibernate")
|
||||
actions.push("reboot", "poweroff")
|
||||
if (selectedIndex < actions.length) {
|
||||
const action = actions[selectedIndex]
|
||||
hide()
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex + 1) % optionCount
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_P:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex + 1) % optionCount
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 320
|
||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
||||
height: implicitHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Options")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 150
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
visible: showLogout
|
||||
color: {
|
||||
if (selectedIndex === 0) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (logoutArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === 0 ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "logout"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Log Out")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const suspendIdx = showLogout ? 1 : 0
|
||||
if (selectedIndex === suspendIdx) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (suspendArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === (showLogout ? 1 : 0) ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === (showLogout ? 1 : 0) ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Suspend")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.suspend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const hibernateIdx = showLogout ? 2 : 1
|
||||
if (selectedIndex === hibernateIdx) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (hibernateArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === (showLogout ? 2 : 1) ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === (showLogout ? 2 : 1) ? 1 : 0
|
||||
visible: SessionService.hibernateSupported
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "ac_unit"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hibernate")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hibernateArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.hibernate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
let rebootIdx = showLogout ? 3 : 2
|
||||
if (!SessionService.hibernateSupported) rebootIdx--
|
||||
if (selectedIndex === rebootIdx) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (rebootArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: {
|
||||
let rebootIdx = showLogout ? 3 : 2
|
||||
if (!SessionService.hibernateSupported) rebootIdx--
|
||||
return selectedIndex === rebootIdx ? Theme.primary : "transparent"
|
||||
}
|
||||
border.width: {
|
||||
let rebootIdx = showLogout ? 3 : 2
|
||||
if (!SessionService.hibernateSupported) rebootIdx--
|
||||
return selectedIndex === rebootIdx ? 1 : 0
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "restart_alt"
|
||||
size: Theme.iconSize
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reboot")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.reboot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
let powerOffIdx = showLogout ? 4 : 3
|
||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
||||
if (selectedIndex === powerOffIdx) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (powerOffArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: {
|
||||
let powerOffIdx = showLogout ? 4 : 3
|
||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
||||
return selectedIndex === powerOffIdx ? Theme.primary : "transparent"
|
||||
}
|
||||
border.width: {
|
||||
let powerOffIdx = showLogout ? 4 : 3
|
||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
||||
return selectedIndex === powerOffIdx ? 1 : 0
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "power_settings_new"
|
||||
size: Theme.iconSize
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Off")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerOffArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.poweroff()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
height: Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankListView {
|
||||
id: listView
|
||||
|
||||
property var keyboardController: null
|
||||
property bool keyboardActive: false
|
||||
property bool autoScrollDisabled: false
|
||||
property alias count: listView.count
|
||||
property alias listContentHeight: listView.contentHeight
|
||||
|
||||
clip: true
|
||||
model: NotificationService.groupedNotifications
|
||||
spacing: Theme.spacingL
|
||||
|
||||
onIsUserScrollingChanged: {
|
||||
if (isUserScrolling && keyboardController && keyboardController.keyboardNavigationActive) {
|
||||
autoScrollDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
function enableAutoScroll() {
|
||||
autoScrollDisabled = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: positionPreservationTimer
|
||||
interval: 200
|
||||
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
|
||||
keyboardController.ensureVisible()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotificationEmptyState {
|
||||
visible: listView.count === 0
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onModelChanged: {
|
||||
if (!keyboardController || !keyboardController.keyboardNavigationActive) {
|
||||
return
|
||||
}
|
||||
keyboardController.rebuildFlatNavigation()
|
||||
Qt.callLater(() => {
|
||||
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
|
||||
keyboardController.ensureVisible()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool isExpanded: (NotificationService.expandedGroups[modelData && modelData.key] || false)
|
||||
|
||||
width: ListView.view.width
|
||||
height: notificationCard.height
|
||||
|
||||
NotificationCard {
|
||||
id: notificationCard
|
||||
width: parent.width
|
||||
notificationGroup: modelData
|
||||
keyboardNavigationActive: listView.keyboardActive
|
||||
|
||||
isGroupSelected: {
|
||||
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) {
|
||||
return false
|
||||
}
|
||||
keyboardController.selectionVersion
|
||||
const selection = keyboardController.getCurrentSelection()
|
||||
return selection.type === "group" && selection.groupIndex === index
|
||||
}
|
||||
|
||||
selectedNotificationIndex: {
|
||||
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) {
|
||||
return -1
|
||||
}
|
||||
keyboardController.selectionVersion
|
||||
const selection = keyboardController.getCurrentSelection()
|
||||
return (selection.type === "notification" && selection.groupIndex === index) ? selection.notificationIndex : -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NotificationService
|
||||
|
||||
function onGroupedNotificationsChanged() {
|
||||
if (!keyboardController) {
|
||||
return
|
||||
}
|
||||
|
||||
if (keyboardController.isTogglingGroup) {
|
||||
keyboardController.rebuildFlatNavigation()
|
||||
return
|
||||
}
|
||||
|
||||
keyboardController.rebuildFlatNavigation()
|
||||
|
||||
if (keyboardController.keyboardNavigationActive) {
|
||||
Qt.callLater(() => {
|
||||
if (!autoScrollDisabled) {
|
||||
keyboardController.ensureVisible()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onExpandedGroupsChanged() {
|
||||
if (keyboardController && keyboardController.keyboardNavigationActive) {
|
||||
Qt.callLater(() => {
|
||||
if (!autoScrollDisabled) {
|
||||
keyboardController.ensureVisible()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onExpandedMessagesChanged() {
|
||||
if (keyboardController && keyboardController.keyboardNavigationActive) {
|
||||
Qt.callLater(() => {
|
||||
if (!autoScrollDisabled) {
|
||||
keyboardController.ensureVisible()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
QtObject {
|
||||
id: manager
|
||||
|
||||
property var modelData
|
||||
property int topMargin: 0
|
||||
property int baseNotificationHeight: 120
|
||||
property int maxTargetNotifications: 4
|
||||
property var popupWindows: [] // strong refs to windows (live until exitFinished)
|
||||
property var destroyingWindows: new Set()
|
||||
property Component popupComponent
|
||||
|
||||
popupComponent: Component {
|
||||
NotificationPopup {
|
||||
onEntered: manager._onPopupEntered(this)
|
||||
onExitFinished: manager._onPopupExitFinished(this)
|
||||
}
|
||||
}
|
||||
|
||||
property Connections notificationConnections
|
||||
|
||||
notificationConnections: Connections {
|
||||
function onVisibleNotificationsChanged() {
|
||||
manager._sync(NotificationService.visibleNotifications)
|
||||
}
|
||||
|
||||
target: NotificationService
|
||||
}
|
||||
|
||||
property Timer sweeper
|
||||
|
||||
sweeper: Timer {
|
||||
interval: 500
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
const toRemove = []
|
||||
for (const p of popupWindows) {
|
||||
if (!p) {
|
||||
toRemove.push(p)
|
||||
continue
|
||||
}
|
||||
const isZombie = p.status === Component.Null || (!p.visible && !p.exiting) || (!p.notificationData && !p._isDestroying) || (!p.hasValidData && !p._isDestroying)
|
||||
if (isZombie) {
|
||||
toRemove.push(p)
|
||||
if (p.forceExit) {
|
||||
p.forceExit()
|
||||
} else if (p.destroy) {
|
||||
try {
|
||||
p.destroy()
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toRemove.length) {
|
||||
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1)
|
||||
const survivors = _active().sort((a, b) => a.screenY - b.screenY)
|
||||
for (let k = 0; k < survivors.length; ++k) {
|
||||
survivors[k].screenY = topMargin + k * baseNotificationHeight
|
||||
}
|
||||
}
|
||||
if (popupWindows.length === 0) {
|
||||
sweeper.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _hasWindowFor(w) {
|
||||
return popupWindows.some(p => p && p.notificationData === w && !p._isDestroying && p.status !== Component.Null)
|
||||
}
|
||||
|
||||
function _isValidWindow(p) {
|
||||
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData
|
||||
}
|
||||
|
||||
function _canMakeRoomFor(wrapper) {
|
||||
const activeWindows = _active()
|
||||
if (activeWindows.length < maxTargetNotifications) {
|
||||
return true
|
||||
}
|
||||
if (!wrapper || !wrapper.notification) {
|
||||
return false
|
||||
}
|
||||
const incomingUrgency = wrapper.notification.urgency || 0
|
||||
for (const p of activeWindows) {
|
||||
if (!p.notificationData || !p.notificationData.notification) {
|
||||
continue
|
||||
}
|
||||
const existingUrgency = p.notificationData.notification.urgency || 0
|
||||
if (existingUrgency < incomingUrgency) {
|
||||
return true
|
||||
}
|
||||
if (existingUrgency === incomingUrgency) {
|
||||
const timer = p.notificationData.timer
|
||||
if (timer && !timer.running) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function _makeRoomForNew(wrapper) {
|
||||
const activeWindows = _active()
|
||||
if (activeWindows.length < maxTargetNotifications) {
|
||||
return
|
||||
}
|
||||
const toRemove = _selectPopupToRemove(activeWindows, wrapper)
|
||||
if (toRemove && !toRemove.exiting) {
|
||||
toRemove.notificationData.removedByLimit = true
|
||||
toRemove.notificationData.popup = false
|
||||
if (toRemove.notificationData.timer) {
|
||||
toRemove.notificationData.timer.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _selectPopupToRemove(activeWindows, incomingWrapper) {
|
||||
const incomingUrgency = (incomingWrapper && incomingWrapper.notification) ? incomingWrapper.notification.urgency || 0 : 0
|
||||
const sortedWindows = activeWindows.slice().sort((a, b) => {
|
||||
const aUrgency = (a.notificationData && a.notificationData.notification) ? a.notificationData.notification.urgency || 0 : 0
|
||||
const bUrgency = (b.notificationData && b.notificationData.notification) ? b.notificationData.notification.urgency || 0 : 0
|
||||
if (aUrgency !== bUrgency) {
|
||||
return aUrgency - bUrgency
|
||||
}
|
||||
const aTimer = a.notificationData && a.notificationData.timer
|
||||
const bTimer = b.notificationData && b.notificationData.timer
|
||||
const aRunning = aTimer && aTimer.running
|
||||
const bRunning = bTimer && bTimer.running
|
||||
if (aRunning !== bRunning) {
|
||||
return aRunning ? 1 : -1
|
||||
}
|
||||
return b.screenY - a.screenY
|
||||
})
|
||||
return sortedWindows[0]
|
||||
}
|
||||
|
||||
function _sync(newWrappers) {
|
||||
for (const w of newWrappers) {
|
||||
if (w && !_hasWindowFor(w)) {
|
||||
insertNewestAtTop(w)
|
||||
}
|
||||
}
|
||||
for (const p of popupWindows.slice()) {
|
||||
if (!_isValidWindow(p)) {
|
||||
continue
|
||||
}
|
||||
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) {
|
||||
p.notificationData.removedByLimit = true
|
||||
p.notificationData.popup = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertNewestAtTop(wrapper) {
|
||||
if (!wrapper) {
|
||||
return
|
||||
}
|
||||
for (const p of popupWindows) {
|
||||
if (!_isValidWindow(p)) {
|
||||
continue
|
||||
}
|
||||
if (p.exiting) {
|
||||
continue
|
||||
}
|
||||
p.screenY = p.screenY + baseNotificationHeight
|
||||
}
|
||||
const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : ""
|
||||
const win = popupComponent.createObject(null, {
|
||||
"notificationData": wrapper,
|
||||
"notificationId": notificationId,
|
||||
"screenY": topMargin,
|
||||
"screen": manager.modelData
|
||||
})
|
||||
if (!win) {
|
||||
return
|
||||
}
|
||||
if (!win.hasValidData) {
|
||||
win.destroy()
|
||||
return
|
||||
}
|
||||
popupWindows.push(win)
|
||||
if (!sweeper.running) {
|
||||
sweeper.start()
|
||||
}
|
||||
}
|
||||
|
||||
function _active() {
|
||||
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting)
|
||||
}
|
||||
|
||||
function _bottom() {
|
||||
let b = null
|
||||
let maxY = -1
|
||||
for (const p of _active()) {
|
||||
if (p.screenY > maxY) {
|
||||
maxY = p.screenY
|
||||
b = p
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
function _onPopupEntered(p) {}
|
||||
|
||||
function _onPopupExitFinished(p) {
|
||||
if (!p) {
|
||||
return
|
||||
}
|
||||
const windowId = p.toString()
|
||||
if (destroyingWindows.has(windowId)) {
|
||||
return
|
||||
}
|
||||
destroyingWindows.add(windowId)
|
||||
const i = popupWindows.indexOf(p)
|
||||
if (i !== -1) {
|
||||
popupWindows.splice(i, 1)
|
||||
popupWindows = popupWindows.slice()
|
||||
}
|
||||
if (NotificationService.releaseWrapper && p.notificationData) {
|
||||
NotificationService.releaseWrapper(p.notificationData)
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
if (p && p.destroy) {
|
||||
try {
|
||||
p.destroy()
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
Qt.callLater(() => destroyingWindows.delete(windowId))
|
||||
})
|
||||
const survivors = _active().sort((a, b) => a.screenY - b.screenY)
|
||||
for (let k = 0; k < survivors.length; ++k) {
|
||||
survivors[k].screenY = topMargin + k * baseNotificationHeight
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupAllWindows() {
|
||||
sweeper.stop()
|
||||
for (const p of popupWindows.slice()) {
|
||||
if (p) {
|
||||
try {
|
||||
if (p.forceExit) {
|
||||
p.forceExit()
|
||||
} else if (p.destroy) {
|
||||
p.destroy()
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
popupWindows = []
|
||||
destroyingWindows.clear()
|
||||
}
|
||||
|
||||
onPopupWindowsChanged: {
|
||||
if (popupWindows.length > 0 && !sweeper.running) {
|
||||
sweeper.start()
|
||||
} else if (popupWindows.length === 0 && sweeper.running) {
|
||||
sweeper.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankOSD {
|
||||
id: root
|
||||
|
||||
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
||||
osdHeight: 40 + Theme.spacingS * 2
|
||||
autoHideInterval: 3000
|
||||
enableMouseInteraction: true
|
||||
|
||||
Connections {
|
||||
target: DisplayService
|
||||
function onBrightnessChanged(showOsd) {
|
||||
if (showOsd) {
|
||||
root.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
property int gap: Theme.spacingS
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
color: "transparent"
|
||||
x: parent.gap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
|
||||
return "brightness_medium"
|
||||
} else if (deviceInfo.name.includes("kbd")) {
|
||||
return "keyboard"
|
||||
} else {
|
||||
return "lightbulb"
|
||||
}
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
id: brightnessSlider
|
||||
|
||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||
height: 40
|
||||
x: parent.gap * 2 + Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: {
|
||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (!deviceInfo) return 1
|
||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
||||
if (isExponential) {
|
||||
return 1
|
||||
}
|
||||
return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0
|
||||
}
|
||||
maximum: {
|
||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (!deviceInfo) return 100
|
||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
||||
if (isExponential) {
|
||||
return 100
|
||||
}
|
||||
return deviceInfo.displayMax || 100
|
||||
}
|
||||
enabled: DisplayService.brightnessAvailable
|
||||
showValue: true
|
||||
unit: {
|
||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (!deviceInfo) return "%"
|
||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
||||
if (isExponential) {
|
||||
return "%"
|
||||
}
|
||||
return deviceInfo.class === "ddc" ? "" : "%"
|
||||
}
|
||||
thumbOutlineColor: Theme.surfaceContainer
|
||||
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
||||
|
||||
Component.onCompleted: {
|
||||
if (DisplayService.brightnessAvailable) {
|
||||
value = DisplayService.brightnessLevel
|
||||
}
|
||||
}
|
||||
|
||||
onSliderValueChanged: newValue => {
|
||||
if (DisplayService.brightnessAvailable) {
|
||||
DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true)
|
||||
resetHideTimer()
|
||||
}
|
||||
}
|
||||
|
||||
onContainsMouseChanged: {
|
||||
setChildHovered(containsMouse)
|
||||
}
|
||||
|
||||
onSliderDragFinished: finalValue => {
|
||||
if (DisplayService.brightnessAvailable) {
|
||||
DisplayService.setBrightness(finalValue, DisplayService.lastIpcDevice, true)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DisplayService
|
||||
|
||||
function onBrightnessChanged(showOsd) {
|
||||
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
|
||||
brightnessSlider.value = DisplayService.brightnessLevel
|
||||
}
|
||||
}
|
||||
|
||||
function onDeviceSwitched() {
|
||||
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
|
||||
brightnessSlider.value = DisplayService.brightnessLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankOSD {
|
||||
id: root
|
||||
|
||||
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
||||
osdHeight: 40 + Theme.spacingS * 2
|
||||
autoHideInterval: 3000
|
||||
enableMouseInteraction: true
|
||||
|
||||
Connections {
|
||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
if (!AudioService.suppressOSD) {
|
||||
root.show()
|
||||
}
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
if (!AudioService.suppressOSD) {
|
||||
root.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AudioService
|
||||
|
||||
function onSinkChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
root.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
property int gap: Theme.spacingS
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
color: "transparent"
|
||||
x: parent.gap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
|
||||
size: Theme.iconSize
|
||||
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: muteButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
AudioService.toggleMute()
|
||||
}
|
||||
onContainsMouseChanged: {
|
||||
setChildHovered(containsMouse || volumeSlider.containsMouse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
id: volumeSlider
|
||||
|
||||
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
||||
readonly property real displayPercent: actualVolumePercent
|
||||
|
||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||
height: 40
|
||||
x: parent.gap * 2 + Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
enabled: AudioService.sink && AudioService.sink.audio
|
||||
showValue: true
|
||||
unit: "%"
|
||||
thumbOutlineColor: Theme.surfaceContainer
|
||||
valueOverride: displayPercent
|
||||
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
||||
|
||||
Component.onCompleted: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
||||
}
|
||||
}
|
||||
|
||||
onSliderValueChanged: newValue => {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.suppressOSD = true
|
||||
AudioService.sink.audio.volume = newValue / 100
|
||||
AudioService.suppressOSD = false
|
||||
resetHideTimer()
|
||||
}
|
||||
}
|
||||
|
||||
onContainsMouseChanged: {
|
||||
setChildHovered(containsMouse || muteButton.containsMouse)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
if (volumeSlider && !volumeSlider.pressed) {
|
||||
volumeSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOsdShown: {
|
||||
if (AudioService.sink && AudioService.sink.audio && contentLoader.item) {
|
||||
const slider = contentLoader.item.children[0].children[1]
|
||||
if (slider) {
|
||||
slider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var popoutTarget: null
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property bool isVerticalOrientation: axis?.isVertical ?? false
|
||||
property bool isFirst: false
|
||||
property bool isLast: false
|
||||
property real sectionSpacing: 0
|
||||
property bool isLeftBarEdge: false
|
||||
property bool isRightBarEdge: false
|
||||
property bool isTopBarEdge: false
|
||||
property bool isBottomBarEdge: false
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
|
||||
readonly property real visualWidth: isVerticalOrientation ? widgetThickness : (contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0)
|
||||
readonly property real visualHeight: isVerticalOrientation ? (contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0) : widgetThickness
|
||||
readonly property alias visualContent: visualContent
|
||||
readonly property real barEdgeExtension: 1000
|
||||
readonly property real gapExtension: sectionSpacing
|
||||
readonly property real leftMargin: !isVerticalOrientation ? (isLeftBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
|
||||
readonly property real rightMargin: !isVerticalOrientation ? (isRightBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
|
||||
readonly property real topMargin: isVerticalOrientation ? (isTopBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
|
||||
readonly property real bottomMargin: isVerticalOrientation ? (isBottomBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
|
||||
width: isVerticalOrientation ? barThickness : visualWidth
|
||||
height: isVerticalOrientation ? visualHeight : barThickness
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: root.visualWidth
|
||||
height: root.visualHeight
|
||||
anchors.centerIn: parent
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.dankBarNoBackground) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
const isHovered = mouseArea.containsMouse || (root.isHovered ?? false)
|
||||
const baseColor = isHovered ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
z: -1
|
||||
x: -root.leftMargin
|
||||
y: -root.topMargin
|
||||
width: root.width + root.leftMargin + root.rightMargin
|
||||
height: root.height + root.topMargin + root.bottomMargin
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onPressed: function (mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
return
|
||||
}
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string pluginId
|
||||
property var pluginService: null
|
||||
default property list<QtObject> content
|
||||
|
||||
signal settingChanged()
|
||||
|
||||
property var variants: []
|
||||
property alias variantsModel: variantsListModel
|
||||
|
||||
implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
readonly property bool hasPermission: {
|
||||
if (!pluginService || !pluginId) return true
|
||||
const allPlugins = pluginService.availablePlugins
|
||||
const plugin = allPlugins[pluginId]
|
||||
if (!plugin) return true
|
||||
const permissions = Array.isArray(plugin.permissions) ? plugin.permissions : []
|
||||
return permissions.indexOf("settings_write") !== -1
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadVariants()
|
||||
}
|
||||
|
||||
onPluginServiceChanged: {
|
||||
if (pluginService) {
|
||||
loadVariants()
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const child = content[i]
|
||||
if (child.loadValue) {
|
||||
child.loadValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onContentChanged: {
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const item = content[i]
|
||||
if (item instanceof Item) {
|
||||
item.parent = settingsColumn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedPluginId) {
|
||||
if (changedPluginId === pluginId) {
|
||||
loadVariants()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadVariants() {
|
||||
if (!pluginService || !pluginId) {
|
||||
variants = []
|
||||
return
|
||||
}
|
||||
variants = pluginService.getPluginVariants(pluginId)
|
||||
syncVariantsToModel()
|
||||
}
|
||||
|
||||
function syncVariantsToModel() {
|
||||
variantsListModel.clear()
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
variantsListModel.append(variants[i])
|
||||
}
|
||||
}
|
||||
|
||||
onVariantsChanged: {
|
||||
syncVariantsToModel()
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: variantsListModel
|
||||
}
|
||||
|
||||
function createVariant(variantName, variantConfig) {
|
||||
if (!pluginService || !pluginId) {
|
||||
return null
|
||||
}
|
||||
return pluginService.createPluginVariant(pluginId, variantName, variantConfig)
|
||||
}
|
||||
|
||||
function removeVariant(variantId) {
|
||||
if (!pluginService || !pluginId) {
|
||||
return
|
||||
}
|
||||
pluginService.removePluginVariant(pluginId, variantId)
|
||||
}
|
||||
|
||||
function updateVariant(variantId, variantConfig) {
|
||||
if (!pluginService || !pluginId) {
|
||||
return
|
||||
}
|
||||
pluginService.updatePluginVariant(pluginId, variantId, variantConfig)
|
||||
}
|
||||
|
||||
function saveValue(key, value) {
|
||||
if (!pluginService) {
|
||||
return
|
||||
}
|
||||
if (!hasPermission) {
|
||||
console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission")
|
||||
return
|
||||
}
|
||||
if (pluginService.savePluginData) {
|
||||
pluginService.savePluginData(pluginId, key, value)
|
||||
settingChanged()
|
||||
}
|
||||
}
|
||||
|
||||
function loadValue(key, defaultValue) {
|
||||
if (pluginService && pluginService.loadPluginData) {
|
||||
return pluginService.loadPluginData(pluginId, key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
function findFlickable(item) {
|
||||
var current = item?.parent
|
||||
while (current) {
|
||||
if (current.contentY !== undefined && current.contentHeight !== undefined) {
|
||||
return current
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function ensureItemVisible(item) {
|
||||
if (!item) return
|
||||
|
||||
var flickable = findFlickable(root)
|
||||
if (!flickable) return
|
||||
|
||||
var itemGlobalY = item.mapToItem(null, 0, 0).y
|
||||
var itemHeight = item.height
|
||||
var flickableGlobalY = flickable.mapToItem(null, 0, 0).y
|
||||
var viewportHeight = flickable.height
|
||||
|
||||
var itemRelativeY = itemGlobalY - flickableGlobalY
|
||||
var viewportTop = 0
|
||||
var viewportBottom = viewportHeight
|
||||
|
||||
if (itemRelativeY < viewportTop) {
|
||||
flickable.contentY = Math.max(0, flickable.contentY - (viewportTop - itemRelativeY) - Theme.spacingL)
|
||||
} else if (itemRelativeY + itemHeight > viewportBottom) {
|
||||
flickable.contentY = Math.min(
|
||||
flickable.contentHeight - viewportHeight,
|
||||
flickable.contentY + (itemRelativeY + itemHeight - viewportBottom) + Theme.spacingL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: errorText
|
||||
visible: pluginService && !root.hasPermission
|
||||
anchors.fill: parent
|
||||
text: I18n.tr("This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json")
|
||||
color: Theme.error
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
visible: root.hasPermission
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,622 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: dockTab
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingL
|
||||
clip: true
|
||||
contentHeight: mainColumn.height
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
// Dock Position
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dockPositionSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: dockPositionSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "swap_vert"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: positionText
|
||||
text: I18n.tr("Dock Position")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - positionText.width - positionButtonGroup.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: positionButtonGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
model: ["Top", "Bottom", "Left", "Right"]
|
||||
currentIndex: {
|
||||
switch (SettingsData.dockPosition) {
|
||||
case SettingsData.Position.Top: return 0
|
||||
case SettingsData.Position.Bottom: return 1
|
||||
case SettingsData.Position.Left: return 2
|
||||
case SettingsData.Position.Right: return 3
|
||||
default: return 1
|
||||
}
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected) {
|
||||
switch (index) {
|
||||
case 0: SettingsData.setDockPosition(SettingsData.Position.Top); break
|
||||
case 1: SettingsData.setDockPosition(SettingsData.Position.Bottom); break
|
||||
case 2: SettingsData.setDockPosition(SettingsData.Position.Left); break
|
||||
case 3: SettingsData.setDockPosition(SettingsData.Position.Right); break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Visibility Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dockVisibilitySection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: dockVisibilitySection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "dock_to_bottom"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- enableToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show Dock")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display a dock with pinned and running applications that can be positioned at the top, bottom, left, or right edge of the screen")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: enableToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.showDock
|
||||
onToggled: checked => {
|
||||
SettingsData.setShowDock(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
visible: SettingsData.showDock
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
DankIcon {
|
||||
name: "visibility_off"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- autoHideToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Auto-hide Dock")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hide the dock when not in use and reveal it when hovering near the dock area")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: autoHideToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.dockAutoHide
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dockAutoHide", checked)
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
visible: CompositorService.isNiri
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: CompositorService.isNiri
|
||||
|
||||
DankIcon {
|
||||
name: "fullscreen"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- overviewToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show on Overview")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Always show the dock when niri's overview is open")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: overviewToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.dockOpenOnOverview
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dockOpenOnOverview", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group by App
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: groupByAppSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: groupByAppSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "apps"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- groupByAppToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Group by App")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Group multiple windows of the same app together with a window count indicator")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: groupByAppToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.dockGroupByApp
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dockGroupByApp", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Indicator Style Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: indicatorStyleSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: indicatorStyleSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "fiber_manual_record"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: indicatorStyleText
|
||||
text: I18n.tr("Indicator Style")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - indicatorStyleText.width - indicatorStyleButtonGroup.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: indicatorStyleButtonGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
model: ["Circle", "Line"]
|
||||
currentIndex: SettingsData.dockIndicatorStyle === "circle" ? 0 : 1
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected) {
|
||||
SettingsData.set("dockIndicatorStyle", index === 0 ? "circle" : "line")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icon Size Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: iconSizeSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 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.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockIconSize", newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Spacing Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dockSpacingSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: dockSpacingSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "space_bar"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Spacing")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Padding")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockSpacing
|
||||
minimum: 0
|
||||
maximum: 32
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockSpacing",
|
||||
newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Height to Edge Gap (Exclusive Zone)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockBottomGap
|
||||
minimum: -100
|
||||
maximum: 100
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockBottomGap",
|
||||
newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Margin")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockMargin
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockMargin", newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Transparency Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: transparencySection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: transparencySection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "opacity"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Dock Transparency")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 32
|
||||
value: Math.round(SettingsData.dockTransparency * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockTransparency",
|
||||
newValue / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,754 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: recentAppsTab
|
||||
|
||||
FileBrowserModal {
|
||||
id: logoFileBrowser
|
||||
browserTitle: I18n.tr("Select Launcher Logo")
|
||||
browserIcon: "image"
|
||||
browserType: "generic"
|
||||
filterExtensions: ["*.svg", "*.png", "*.jpg", "*.jpeg", "*.webp"]
|
||||
onFileSelected: path => {
|
||||
SettingsData.set("launcherLogoCustomPath", path.replace("file://", ""))
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingL
|
||||
clip: true
|
||||
contentHeight: mainColumn.height
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: launcherLogoSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: launcherLogoSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "apps"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launcher Button Logo")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Choose the logo displayed on the launcher button in DankBar")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: logoModeGroup.height
|
||||
|
||||
DankButtonGroup {
|
||||
id: logoModeGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
model: {
|
||||
const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo"), I18n.tr("Dank")]
|
||||
if (CompositorService.isNiri) {
|
||||
modes.push("niri")
|
||||
} else if (CompositorService.isHyprland) {
|
||||
modes.push("Hyprland")
|
||||
} else if (CompositorService.isDwl) {
|
||||
modes.push("mango")
|
||||
} else if (CompositorService.isSway) {
|
||||
modes.push("Sway")
|
||||
} else {
|
||||
modes.push(I18n.tr("Compositor"))
|
||||
}
|
||||
modes.push(I18n.tr("Custom"))
|
||||
return modes
|
||||
}
|
||||
currentIndex: {
|
||||
if (SettingsData.launcherLogoMode === "apps") return 0
|
||||
if (SettingsData.launcherLogoMode === "os") return 1
|
||||
if (SettingsData.launcherLogoMode === "dank") return 2
|
||||
if (SettingsData.launcherLogoMode === "compositor") return 3
|
||||
if (SettingsData.launcherLogoMode === "custom") return 4
|
||||
return 0
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected) return
|
||||
if (index === 0) {
|
||||
SettingsData.set("launcherLogoMode", "apps")
|
||||
} else if (index === 1) {
|
||||
SettingsData.set("launcherLogoMode", "os")
|
||||
} else if (index === 2) {
|
||||
SettingsData.set("launcherLogoMode", "dank")
|
||||
} else if (index === 3) {
|
||||
SettingsData.set("launcherLogoMode", "compositor")
|
||||
} else if (index === 4) {
|
||||
SettingsData.set("launcherLogoMode", "custom")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
visible: SettingsData.launcherLogoMode === "custom"
|
||||
opacity: visible ? 1 : 0
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width - selectButton.width - Theme.spacingM
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: SettingsData.launcherLogoCustomPath || I18n.tr("Select an image file...")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: SettingsData.launcherLogoCustomPath ? Theme.surfaceText : Theme.outlineButton
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: selectButton
|
||||
iconName: "folder_open"
|
||||
width: 36
|
||||
height: 36
|
||||
onClicked: logoFileBrowser.open()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
visible: SettingsData.launcherLogoMode !== "apps"
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Color Override")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButtonGroup {
|
||||
id: colorModeGroup
|
||||
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
||||
currentIndex: {
|
||||
const override = SettingsData.launcherLogoColorOverride
|
||||
if (override === "") return 0
|
||||
if (override === "primary") return 1
|
||||
if (override === "surface") return 2
|
||||
return 3
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected) return
|
||||
if (index === 0) {
|
||||
SettingsData.set("launcherLogoColorOverride", "")
|
||||
} else if (index === 1) {
|
||||
SettingsData.set("launcherLogoColorOverride", "primary")
|
||||
} else if (index === 2) {
|
||||
SettingsData.set("launcherLogoColorOverride", "surface")
|
||||
} else if (index === 3) {
|
||||
const currentOverride = SettingsData.launcherLogoColorOverride
|
||||
const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface"
|
||||
if (isPreset) {
|
||||
SettingsData.set("launcherLogoColorOverride", "#ffffff")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: {
|
||||
const override = SettingsData.launcherLogoColorOverride
|
||||
return override !== "" && override !== "primary" && override !== "surface"
|
||||
}
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
color: {
|
||||
const override = SettingsData.launcherLogoColorOverride
|
||||
if (override !== "" && override !== "primary" && override !== "surface") {
|
||||
return override
|
||||
}
|
||||
return "#ffffff"
|
||||
}
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (PopoutService.colorPickerModal) {
|
||||
PopoutService.colorPickerModal.selectedColor = SettingsData.launcherLogoColorOverride
|
||||
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Launcher Logo Color")
|
||||
PopoutService.colorPickerModal.onColorSelectedCallback = function(selectedColor) {
|
||||
SettingsData.set("launcherLogoColorOverride", selectedColor)
|
||||
}
|
||||
PopoutService.colorPickerModal.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: 120
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Size Offset")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: 100
|
||||
height: 20
|
||||
minimum: -12
|
||||
maximum: 12
|
||||
value: SettingsData.launcherLogoSizeOffset
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("launcherLogoSizeOffset", newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: customControlsFlow.height
|
||||
visible: {
|
||||
const override = SettingsData.launcherLogoColorOverride
|
||||
return override !== "" && override !== "primary" && override !== "surface"
|
||||
}
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: customControlsFlow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: 120
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Brightness")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: 100
|
||||
height: 20
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
value: Math.round(SettingsData.launcherLogoBrightness * 100)
|
||||
unit: "%"
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("launcherLogoBrightness", newValue / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: 120
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Contrast")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: 100
|
||||
height: 20
|
||||
minimum: 0
|
||||
maximum: 200
|
||||
value: Math.round(SettingsData.launcherLogoContrast * 100)
|
||||
unit: "%"
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("launcherLogoContrast", newValue / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: 120
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Invert on mode change")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: 32
|
||||
height: 18
|
||||
checked: SettingsData.launcherLogoColorInvertOnMode
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
onToggled: checked => {
|
||||
SettingsData.set("launcherLogoColorInvertOnMode", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: launchPrefixSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: launchPrefixSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "terminal"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch Prefix")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Add a custom prefix to all application launches. This can be used for things like 'uwsm-app', 'systemd-run', or other command wrappers.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
text: SettingsData.launchPrefix
|
||||
placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')")
|
||||
onTextEdited: {
|
||||
SettingsData.set("launchPrefix", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: sortingSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
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.set("sortAppsAlphabetically", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: recentlyUsedSection
|
||||
|
||||
property var rankedAppsModel: {
|
||||
var apps = []
|
||||
for (var appId in (AppUsageHistoryData.appUsageRanking
|
||||
|| {})) {
|
||||
var appData = (AppUsageHistoryData.appUsageRanking
|
||||
|| {})[appId]
|
||||
apps.push({
|
||||
"id": appId,
|
||||
"name": appData.name,
|
||||
"exec": appData.exec,
|
||||
"icon": appData.icon,
|
||||
"comment": appData.comment,
|
||||
"usageCount": appData.usageCount,
|
||||
"lastUsed": appData.lastUsed
|
||||
})
|
||||
}
|
||||
apps.sort(function (a, b) {
|
||||
if (a.usageCount !== b.usageCount)
|
||||
return b.usageCount - a.usageCount
|
||||
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
return apps.slice(0, 20)
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "history"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Recently Used Apps")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.children[0].width
|
||||
- parent.children[1].width
|
||||
- clearAllButton.width - Theme.spacingM * 3
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: clearAllButton
|
||||
|
||||
iconName: "delete_sweep"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.error
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
AppUsageHistoryData.appUsageRanking = {}
|
||||
AppUsageHistoryData.saveSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Apps are ordered by usage frequency, then last used, then alphabetically.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Column {
|
||||
id: rankedAppsList
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: recentlyUsedSection.rankedAppsModel
|
||||
|
||||
delegate: Rectangle {
|
||||
width: rankedAppsList.width
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
||||
Theme.surfaceContainer.g,
|
||||
Theme.surfaceContainer.b, 0.3)
|
||||
border.color: Qt.rgba(Theme.outline.r,
|
||||
Theme.outline.g,
|
||||
Theme.outline.b, 0.1)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: (index + 1).toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primary
|
||||
width: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = "image://icon/application-x-executable"
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
|| "Unknown App"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!modelData.lastUsed)
|
||||
return "Never used"
|
||||
|
||||
var date = new Date(modelData.lastUsed)
|
||||
var now = new Date()
|
||||
var diffMs = now - date
|
||||
var diffMins = Math.floor(
|
||||
diffMs / (1000 * 60))
|
||||
var diffHours = Math.floor(
|
||||
diffMs / (1000 * 60 * 60))
|
||||
var diffDays = Math.floor(
|
||||
diffMs / (1000 * 60 * 60 * 24))
|
||||
if (diffMins < 1)
|
||||
return I18n.tr("Last launched just now")
|
||||
|
||||
if (diffMins < 60)
|
||||
return I18n.tr("Last launched %1 minute%2 ago")
|
||||
.arg(diffMins)
|
||||
.arg(diffMins === 1 ? "" : "s")
|
||||
|
||||
if (diffHours < 24)
|
||||
return I18n.tr("Last launched %1 hour%2 ago")
|
||||
.arg(diffHours)
|
||||
.arg(diffHours === 1 ? "" : "s")
|
||||
|
||||
if (diffDays < 7)
|
||||
return I18n.tr("Last launched %1 day%2 ago")
|
||||
.arg(diffDays)
|
||||
.arg(diffDays === 1 ? "" : "s")
|
||||
|
||||
return I18n.tr("Last launched %1")
|
||||
.arg(date.toLocaleDateString())
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
circular: true
|
||||
iconName: "close"
|
||||
iconSize: 16
|
||||
iconColor: Theme.error
|
||||
onClicked: {
|
||||
var currentRanking = Object.assign(
|
||||
{},
|
||||
AppUsageHistoryData.appUsageRanking
|
||||
|| {})
|
||||
delete currentRanking[modelData.id]
|
||||
AppUsageHistoryData.appUsageRanking = currentRanking
|
||||
AppUsageHistoryData.saveSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: recentlyUsedSection.rankedAppsModel.length
|
||||
=== 0 ? "No apps have been launched yet." : ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: recentlyUsedSection.rankedAppsModel.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,669 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property var allPlugins: []
|
||||
property string searchQuery: ""
|
||||
property var filteredPlugins: []
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool isLoading: false
|
||||
property var parentModal: null
|
||||
|
||||
width: 600
|
||||
height: 650
|
||||
allowStacking: true
|
||||
backgroundOpacity: 0
|
||||
closeOnEscapeKey: false
|
||||
|
||||
function updateFilteredPlugins() {
|
||||
var filtered = []
|
||||
var query = searchQuery ? searchQuery.toLowerCase() : ""
|
||||
|
||||
for (var i = 0; i < allPlugins.length; i++) {
|
||||
var plugin = allPlugins[i]
|
||||
var isFirstParty = plugin.firstParty || false
|
||||
|
||||
if (!SessionData.showThirdPartyPlugins && !isFirstParty) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (query.length > 0) {
|
||||
var name = plugin.name ? plugin.name.toLowerCase() : ""
|
||||
var description = plugin.description ? plugin.description.toLowerCase() : ""
|
||||
var author = plugin.author ? plugin.author.toLowerCase() : ""
|
||||
|
||||
if (name.indexOf(query) !== -1 ||
|
||||
description.indexOf(query) !== -1 ||
|
||||
author.indexOf(query) !== -1) {
|
||||
filtered.push(plugin)
|
||||
}
|
||||
} else {
|
||||
filtered.push(plugin)
|
||||
}
|
||||
}
|
||||
|
||||
filteredPlugins = filtered
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredPlugins.length === 0) return
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredPlugins.length - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredPlugins.length === 0) return
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, -1)
|
||||
if (selectedIndex === -1) {
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
}
|
||||
|
||||
function installPlugin(pluginName) {
|
||||
ToastService.showInfo("Installing plugin: " + pluginName)
|
||||
DMSService.install(pluginName, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError("Install failed: " + response.error)
|
||||
} else {
|
||||
ToastService.showInfo("Plugin installed: " + pluginName)
|
||||
PluginService.scanPlugins()
|
||||
refreshPlugins()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function refreshPlugins() {
|
||||
isLoading = true
|
||||
DMSService.listPlugins()
|
||||
if (DMSService.apiVersion >= 8) {
|
||||
DMSService.listInstalled()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = false
|
||||
}
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
Qt.callLater(() => {
|
||||
if (parentModal.modalFocusScope) {
|
||||
parentModal.modalFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
refreshPlugins()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentLoader
|
||||
function onLoaded() {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onDialogClosed: () => {
|
||||
allPlugins = []
|
||||
searchQuery = ""
|
||||
filteredPlugins = []
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
onBackgroundClicked: () => {
|
||||
hide()
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: browserKeyHandler
|
||||
property alias searchField: browserSearchField
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
browserSearchField.forceActiveFocus()
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
root.selectPrevious()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: browserContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
|
||||
Item {
|
||||
id: headerArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: Math.max(headerIcon.height, headerText.height, refreshButton.height, closeButton.height)
|
||||
|
||||
DankIcon {
|
||||
id: headerIcon
|
||||
name: "store"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: headerText
|
||||
text: I18n.tr("Browse Plugins")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.left: headerIcon.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankButton {
|
||||
id: thirdPartyButton
|
||||
text: SessionData.showThirdPartyPlugins ? "Hide 3rd Party" : "Show 3rd Party"
|
||||
iconName: SessionData.showThirdPartyPlugins ? "visibility_off" : "visibility"
|
||||
height: 28
|
||||
onClicked: {
|
||||
if (SessionData.showThirdPartyPlugins) {
|
||||
SessionData.setShowThirdPartyPlugins(false)
|
||||
root.updateFilteredPlugins()
|
||||
} else {
|
||||
thirdPartyConfirmModal.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: refreshButton
|
||||
iconName: "refresh"
|
||||
iconSize: 18
|
||||
iconColor: Theme.primary
|
||||
visible: !root.isLoading
|
||||
onClicked: root.refreshPlugins()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: closeButton
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.outline
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: descriptionText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: headerArea.bottom
|
||||
anchors.topMargin: Theme.spacingM
|
||||
text: I18n.tr("Install plugins from the DMS plugin registry")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: browserSearchField
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: descriptionText.bottom
|
||||
anchors.topMargin: Theme.spacingM
|
||||
height: 48
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
placeholderText: I18n.tr("Search plugins...")
|
||||
text: root.searchQuery
|
||||
focus: true
|
||||
ignoreLeftRightKeys: true
|
||||
keyForwardTargets: [browserKeyHandler]
|
||||
onTextEdited: {
|
||||
root.searchQuery = text
|
||||
root.updateFilteredPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: listArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: browserSearchField.bottom
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.isLoading
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "sync"
|
||||
size: 48
|
||||
color: Theme.primary
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
RotationAnimator on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
running: root.isLoading
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Loading plugins...")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: pluginBrowserList
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
model: ScriptModel {
|
||||
values: root.filteredPlugins
|
||||
}
|
||||
clip: true
|
||||
visible: !root.isLoading
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: browserScrollbar
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: pluginBrowserList.width
|
||||
height: pluginDelegateColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
|
||||
property bool isInstalled: modelData.installed || false
|
||||
property bool isFirstParty: modelData.firstParty || false
|
||||
color: isSelected ? Theme.primarySelected :
|
||||
Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.3)
|
||||
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: isSelected ? 2 : 1
|
||||
|
||||
Column {
|
||||
id: pluginDelegateColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || "extension"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - installButton.width - Theme.spacingM
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 16
|
||||
width: firstPartyText.implicitWidth + Theme.spacingXS * 2
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4)
|
||||
border.width: 1
|
||||
visible: isFirstParty
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: firstPartyText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("official")
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 16
|
||||
width: thirdPartyText.implicitWidth + Theme.spacingXS * 2
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.15)
|
||||
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.4)
|
||||
border.width: 1
|
||||
visible: !isFirstParty
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: thirdPartyText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("3rd party")
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.warning
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const author = "by " + (modelData.author || "Unknown")
|
||||
const source = modelData.repo ? ` • <a href="${modelData.repo}" style="text-decoration:none; color:${Theme.primary};">source</a>` : ""
|
||||
return author + source
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
linkColor: Theme.primary
|
||||
textFormat: Text.RichText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: installButton
|
||||
width: 80
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: isInstalled ? Theme.surfaceVariant : Theme.primary
|
||||
opacity: isInstalled ? 1 : (installMouseArea.containsMouse ? 0.9 : 1)
|
||||
border.width: isInstalled ? 1 : 0
|
||||
border.color: Theme.outline
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: isInstalled ? "check" : "download"
|
||||
size: 14
|
||||
color: isInstalled ? Theme.surfaceText : Theme.surface
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: isInstalled ? "Installed" : "Install"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: isInstalled ? Theme.surfaceText : Theme.surface
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: installMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: isInstalled ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
enabled: !isInstalled
|
||||
onClicked: {
|
||||
if (!isInstalled) {
|
||||
root.installPlugin(modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.description || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: modelData.description && modelData.description.length > 0
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: modelData.capabilities && modelData.capabilities.length > 0
|
||||
|
||||
Repeater {
|
||||
model: modelData.capabilities || []
|
||||
|
||||
Rectangle {
|
||||
height: 18
|
||||
width: capabilityText.implicitWidth + Theme.spacingXS * 2
|
||||
radius: 9
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: capabilityText
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: listArea
|
||||
text: I18n.tr("No plugins found")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: !root.isLoading && root.filteredPlugins.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankModal {
|
||||
id: thirdPartyConfirmModal
|
||||
|
||||
width: 500
|
||||
height: 300
|
||||
allowStacking: true
|
||||
backgroundOpacity: 0.4
|
||||
closeOnEscapeKey: true
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
thirdPartyConfirmModal.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "warning"
|
||||
size: Theme.iconSize
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Third-Party Plugin Warning")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Third-party plugins are created by the community and are not officially supported by DankMaterialShell.\n\nThese plugins may pose security and privacy risks - install at your own risk.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• Plugins may contain bugs or security issues")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• Review code before installation when possible")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• Install only from trusted sources")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - parent.spacing * 3 - y
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Cancel")
|
||||
iconName: "close"
|
||||
onClicked: thirdPartyConfirmModal.close()
|
||||
}
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("I Understand")
|
||||
iconName: "check"
|
||||
onClicked: {
|
||||
SessionData.setShowThirdPartyPlugins(true)
|
||||
root.updateFilteredPlugins()
|
||||
thirdPartyConfirmModal.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property string title: ""
|
||||
property string iconName: ""
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property bool expanded: false
|
||||
property bool collapsible: true
|
||||
property bool lazyLoad: true
|
||||
|
||||
width: parent.width
|
||||
spacing: expanded ? Theme.spacingM : 0
|
||||
Component.onCompleted: {
|
||||
if (!collapsible)
|
||||
expanded = true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
width: parent.width
|
||||
height: headerRow.height
|
||||
enabled: collapsible
|
||||
hoverEnabled: collapsible
|
||||
onClicked: {
|
||||
if (collapsible)
|
||||
expanded = !expanded
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: parent.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.08) : "transparent"
|
||||
radius: Theme.radiusS
|
||||
}
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.collapsible ? (root.expanded ? "expand_less" : "expand_more") : root.iconName
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.fast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.collapsible
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
visible: expanded || !collapsible
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
width: parent.width
|
||||
active: lazyLoad ? expanded || !collapsible : true
|
||||
visible: expanded || !collapsible
|
||||
asynchronous: true
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,346 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property var allWidgets: []
|
||||
property string targetSection: ""
|
||||
property string searchQuery: ""
|
||||
property var filteredWidgets: []
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property Component widgetSelectionContent
|
||||
property var parentModal: null
|
||||
|
||||
signal widgetSelected(string widgetId, string targetSection)
|
||||
|
||||
function updateFilteredWidgets() {
|
||||
if (!searchQuery || searchQuery.length === 0) {
|
||||
filteredWidgets = allWidgets.slice()
|
||||
return
|
||||
}
|
||||
|
||||
var filtered = []
|
||||
var query = searchQuery.toLowerCase()
|
||||
|
||||
for (var i = 0; i < allWidgets.length; i++) {
|
||||
var widget = allWidgets[i]
|
||||
var text = widget.text ? widget.text.toLowerCase() : ""
|
||||
var description = widget.description ? widget.description.toLowerCase() : ""
|
||||
var id = widget.id ? widget.id.toLowerCase() : ""
|
||||
|
||||
if (text.indexOf(query) !== -1 ||
|
||||
description.indexOf(query) !== -1 ||
|
||||
id.indexOf(query) !== -1) {
|
||||
filtered.push(widget)
|
||||
}
|
||||
}
|
||||
|
||||
filteredWidgets = filtered
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
onAllWidgetsChanged: {
|
||||
updateFilteredWidgets()
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredWidgets.length === 0) return
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredWidgets.length - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredWidgets.length === 0) return
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, -1)
|
||||
if (selectedIndex === -1) {
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectWidget() {
|
||||
if (selectedIndex >= 0 && selectedIndex < filteredWidgets.length) {
|
||||
var widget = filteredWidgets[selectedIndex]
|
||||
root.widgetSelected(widget.id, root.targetSection)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = false
|
||||
}
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
Qt.callLater(() => {
|
||||
if (parentModal && parentModal.modalFocusScope) {
|
||||
parentModal.modalFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
width: 500
|
||||
height: 550
|
||||
allowStacking: true
|
||||
backgroundOpacity: 0
|
||||
closeOnEscapeKey: false
|
||||
onDialogClosed: () => {
|
||||
allWidgets = []
|
||||
targetSection = ""
|
||||
searchQuery = ""
|
||||
filteredWidgets = []
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
Qt.callLater(() => {
|
||||
if (parentModal && parentModal.modalFocusScope) {
|
||||
parentModal.modalFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
return hide()
|
||||
}
|
||||
content: widgetSelectionContent
|
||||
|
||||
widgetSelectionContent: Component {
|
||||
FocusScope {
|
||||
id: widgetKeyHandler
|
||||
property alias searchField: searchField
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
root.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
root.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
root.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (root.keyboardNavigationActive) {
|
||||
root.selectWidget()
|
||||
} else if (root.filteredWidgets.length > 0) {
|
||||
var firstWidget = root.filteredWidgets[0]
|
||||
root.widgetSelected(firstWidget.id, root.targetSection)
|
||||
root.close()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.outline
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
spacing: Theme.spacingM
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingL + 30 // Space for close button
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "add_circle"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Add Widget to ") + root.targetSection + " Section"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Select a widget to add to the ") + root.targetSection.toLowerCase(
|
||||
) + " section of the top bar. You can add multiple instances of the same widget if needed."
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
width: parent.width
|
||||
height: 48
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
placeholderText: ""
|
||||
text: root.searchQuery
|
||||
focus: true
|
||||
ignoreLeftRightKeys: true
|
||||
keyForwardTargets: [widgetKeyHandler]
|
||||
onTextEdited: {
|
||||
root.searchQuery = text
|
||||
updateFilteredWidgets()
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up ||
|
||||
((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: widgetList
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
spacing: Theme.spacingS
|
||||
model: root.filteredWidgets
|
||||
clip: true
|
||||
|
||||
delegate: Rectangle {
|
||||
width: widgetList.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
|
||||
color: isSelected ? Theme.primarySelected :
|
||||
widgetArea.containsMouse ? Theme.primaryHover : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.3)
|
||||
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: isSelected ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM * 3
|
||||
|
||||
StyledText {
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "add"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: widgetArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.widgetSelected(modelData.id,
|
||||
root.targetSection)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,705 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: widgetTweaksTab
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingL
|
||||
clip: true
|
||||
contentHeight: mainColumn.height
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: workspaceSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: workspaceSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "view_module"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Workspace Settings")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Workspace Index Numbers")
|
||||
description: I18n.tr("Show workspace index numbers in the top bar workspace switcher")
|
||||
checked: SettingsData.showWorkspaceIndex
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("showWorkspaceIndex",
|
||||
checked)
|
||||
}
|
||||
}
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Workspace Padding")
|
||||
description: I18n.tr("Always show a minimum of 3 workspaces, even if fewer are available")
|
||||
checked: SettingsData.showWorkspacePadding
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("showWorkspacePadding",
|
||||
checked)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Workspace Apps")
|
||||
description: I18n.tr("Display application icons in workspace indicators")
|
||||
checked: SettingsData.showWorkspaceApps
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("showWorkspaceApps",
|
||||
checked)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width - Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
visible: SettingsData.showWorkspaceApps
|
||||
opacity: visible ? 1 : 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
|
||||
Column {
|
||||
width: 120
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Max apps to show")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: 100
|
||||
height: 28
|
||||
placeholderText: "#ffffff"
|
||||
text: SettingsData.maxWorkspaceIcons
|
||||
maximumLength: 7
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
topPadding: Theme.spacingXS
|
||||
bottomPadding: Theme.spacingXS
|
||||
onEditingFinished: {
|
||||
SettingsData.set("maxWorkspaceIcons", parseInt(text, 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Per-Monitor Workspaces")
|
||||
description: I18n.tr("Show only workspaces belonging to each specific monitor.")
|
||||
checked: SettingsData.workspacesPerMonitor
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("workspacesPerMonitor", checked);
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show All Tags")
|
||||
description: I18n.tr("Show all 9 tags instead of only occupied tags (DWL only)")
|
||||
checked: SettingsData.dwlShowAllTags
|
||||
visible: CompositorService.isDwl
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("dwlShowAllTags", checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: mediaSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: mediaSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "music_note"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Media Player Settings")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Wave Progress Bars")
|
||||
description: I18n.tr("Use animated wave progress bars for media playback")
|
||||
checked: SettingsData.waveProgressEnabled
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("waveProgressEnabled", checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: updaterSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
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.set("updaterCustomCommand", "");
|
||||
SettingsData.set("updaterTerminalAdditionalParams", "");
|
||||
}
|
||||
return SettingsData.set("updaterUseCustomCommand", checked);
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
height: customCommandColumn.implicitHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
|
||||
Column {
|
||||
id: customCommandColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
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.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.updaterCustomCommand) {
|
||||
text = SettingsData.updaterCustomCommand;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("updaterCustomCommand", text.trim());
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => {
|
||||
updaterCustomCommand.forceActiveFocus()
|
||||
mouse.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
height: terminalParamsColumn.implicitHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
|
||||
Column {
|
||||
id: terminalParamsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
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.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.updaterTerminalAdditionalParams) {
|
||||
text = SettingsData.updaterTerminalAdditionalParams;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("updaterTerminalAdditionalParams", text.trim());
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => {
|
||||
updaterTerminalCustomClass.forceActiveFocus()
|
||||
mouse.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: runningAppsSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
|
||||
Column {
|
||||
id: runningAppsSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "apps"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Running Apps Settings")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Running Apps Only In Current Workspace")
|
||||
description: I18n.tr("Show only apps running in current workspace")
|
||||
checked: SettingsData.runningAppsCurrentWorkspace
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("runningAppsCurrentWorkspace",
|
||||
checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: SettingsData.hasNamedWorkspaces()
|
||||
|
||||
Column {
|
||||
id: workspaceIconsSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "label"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Named Workspace Icons")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Configure icons for named workspaces. Icons take priority over numbers when both are enabled.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: SettingsData.getNamedWorkspaces()
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: workspaceIconRow.implicitHeight + Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
||||
Theme.surfaceContainer.g,
|
||||
Theme.surfaceContainer.b, 0.5)
|
||||
border.color: Qt.rgba(Theme.outline.r,
|
||||
Theme.outline.g,
|
||||
Theme.outline.b, 0.3)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: workspaceIconRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "\"" + modelData + "\""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 150
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
DankIconPicker {
|
||||
id: iconPicker
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Component.onCompleted: {
|
||||
var iconData = SettingsData.getWorkspaceNameIcon(
|
||||
modelData)
|
||||
if (iconData) {
|
||||
setIcon(iconData.value,
|
||||
iconData.type)
|
||||
}
|
||||
}
|
||||
|
||||
onIconSelected: (iconName, iconType) => {
|
||||
SettingsData.setWorkspaceNameIcon(
|
||||
modelData, {
|
||||
"type": iconType,
|
||||
"value": iconName
|
||||
})
|
||||
setIcon(iconName,
|
||||
iconType)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onWorkspaceIconsUpdated() {
|
||||
var iconData = SettingsData.getWorkspaceNameIcon(
|
||||
modelData)
|
||||
if (iconData) {
|
||||
iconPicker.setIcon(
|
||||
iconData.value,
|
||||
iconData.type)
|
||||
} else {
|
||||
iconPicker.setIcon("", "icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer
|
||||
border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
|
||||
border.width: 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "close"
|
||||
size: 16
|
||||
color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
SettingsData.removeWorkspaceNameIcon(
|
||||
modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: notificationSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: notificationSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "notifications"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Notification Popups")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
leftPadding: Theme.spacingM
|
||||
rightPadding: Theme.spacingM
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
text: I18n.tr("Popup Position")
|
||||
description: I18n.tr("Choose where notification popups appear on screen")
|
||||
currentValue: {
|
||||
if (SettingsData.notificationPopupPosition === -1) {
|
||||
return "Top Center"
|
||||
}
|
||||
switch (SettingsData.notificationPopupPosition) {
|
||||
case SettingsData.Position.Top:
|
||||
return "Top Right"
|
||||
case SettingsData.Position.Bottom:
|
||||
return "Bottom Left"
|
||||
case SettingsData.Position.Left:
|
||||
return "Top Left"
|
||||
case SettingsData.Position.Right:
|
||||
return "Bottom Right"
|
||||
default:
|
||||
return "Top Right"
|
||||
}
|
||||
}
|
||||
options: ["Top Right", "Top Left", "Top Center", "Bottom Right", "Bottom Left"]
|
||||
onValueChanged: value => {
|
||||
switch (value) {
|
||||
case "Top Right":
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.Top)
|
||||
break
|
||||
case "Top Left":
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.Left)
|
||||
break
|
||||
case "Top Center":
|
||||
SettingsData.set("notificationPopupPosition", -1)
|
||||
break
|
||||
case "Bottom Right":
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.Right)
|
||||
break
|
||||
case "Bottom Left":
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.Bottom)
|
||||
break
|
||||
}
|
||||
SettingsData.sendTestNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: osdRow.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: osdRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "tune"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - osdToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Always Show OSD Percentage")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display volume and brightness percentage values by default in OSD popups")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: osdToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.osdAlwaysShowValue
|
||||
onToggleCompleted: checked => {
|
||||
SettingsData.set("osdAlwaysShowValue", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
224
README.md
224
README.md
@@ -1,34 +1,47 @@
|
||||
# DankMaterialShell (dms)
|
||||
# DankMaterialShell
|
||||
|
||||
<div align="center">
|
||||
<a href="https://danklinux.com">
|
||||
<img src="assets/danklogo2.svg" alt="DankMaterialShell Logo" width="200">
|
||||
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
|
||||
</a>
|
||||
|
||||
### A modern Wayland desktop shell
|
||||
### A modern desktop shell for Wayland
|
||||
|
||||
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
||||
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
||||
|
||||
[](https://danklinux.com/docs)
|
||||
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
||||
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||
[](https://ko-fi.com/avengemediallc)
|
||||
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||
[](https://ko-fi.com/danklinux)
|
||||
|
||||
</div>
|
||||
|
||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hypr.land), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop - all in one cohesive package with a gorgeous interface.
|
||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
||||
|
||||
## Components
|
||||
## Repository Structure
|
||||
|
||||
DankMaterialShell combines two main components:
|
||||
This is a monorepo containing both the shell interface and the core backend services:
|
||||
|
||||
- **[QML/UI Layer](https://github.com/AvengeMedia/DankMaterialShell)** (this repo) - All the visual components, widgets, and shell interface built with Quickshell
|
||||
- **[Go Backend](https://github.com/AvengeMedia/danklinux)** - System integration, IPC, process management, and core services
|
||||
|
||||
---
|
||||
```
|
||||
DankMaterialShell/
|
||||
├── quickshell/ # QML-based shell interface
|
||||
│ ├── Modules/ # UI components (panels, widgets, overlays)
|
||||
│ ├── Services/ # System integration (audio, network, bluetooth)
|
||||
│ ├── Widgets/ # Reusable UI controls
|
||||
│ └── Common/ # Shared resources and themes
|
||||
├── core/ # Go backend and CLI
|
||||
│ ├── cmd/ # dms CLI and dankinstall binaries
|
||||
│ ├── internal/ # System integration, IPC, distro support
|
||||
│ └── pkg/ # Shared packages
|
||||
├── distro/ # Distribution packaging
|
||||
│ ├── fedora/ # Fedora RPM specs
|
||||
│ ├── debian/ # Debian packaging
|
||||
│ └── nix/ # NixOS/home-manager modules
|
||||
└── flake.nix # Nix flake for declarative installation
|
||||
```
|
||||
|
||||
## See it in Action
|
||||
|
||||
@@ -54,166 +67,129 @@ https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Quick Install
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.danklinux.com | sh
|
||||
```
|
||||
|
||||
That's it. One command installs dms and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
|
||||
One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
|
||||
|
||||
**[Manual Installation Guide →](https://danklinux.com/docs/dankmaterialshell/installation)**
|
||||
**[Manual installation guide](https://danklinux.com/docs/dankmaterialshell/installation)**
|
||||
|
||||
---
|
||||
|
||||
## What You Get
|
||||
## Features
|
||||
|
||||
**Dynamic Theming**
|
||||
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (like vscode, vscodium), and more with [matugen](https://github.com/InioX/matugen) and [dank16](https://github.com/AvengeMedia/danklinux/blob/master/internal/dank16/dank16.go).
|
||||
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
|
||||
|
||||
**System Monitoring**
|
||||
Real-time CPU, RAM, GPU metrics and temps with [dgop](https://github.com/AvengeMedia/dgop). Full process list with search and management.
|
||||
Real-time CPU, RAM, GPU metrics and temperatures with [dgop](https://github.com/AvengeMedia/dgop). Process list with search and management.
|
||||
|
||||
**Powerful Launcher**
|
||||
Spotlight-style search for apps, files (via [dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, commands - extensible with plugins.
|
||||
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
|
||||
|
||||
**Control Center**
|
||||
Network, Bluetooth, audio devices, display settings, night mode - all in one clean interface.
|
||||
Unified interface for network, Bluetooth, audio devices, display settings, and night mode.
|
||||
|
||||
**Smart Notifications**
|
||||
Notification center with grouping, rich text support, and keyboard navigation.
|
||||
|
||||
**Media Integration**
|
||||
MPRIS player controls, calendar sync, weather widgets, clipboard history with image previews.
|
||||
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
||||
|
||||
**Complete Session Management**
|
||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, greeter support.
|
||||
**Session Management**
|
||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
||||
|
||||
**Plugin System**
|
||||
Endless customization with the [plugin registry](https://plugins.danklinux.com).
|
||||
|
||||
**TL;DR** - One shell replaces waybar, swaylock, swayidle, mako, fuzzel, polkit and everything else you normally piece together to create a linux desktop.
|
||||
|
||||
---
|
||||
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
||||
|
||||
## Supported Compositors
|
||||
|
||||
DankMaterialShell works best with **[niri](https://github.com/YaLTeR/niri)**, **[Hyprland](https://hyprland.org/)**, **[sway](https://swaywm.org/)**, and **[dwl/MangoWC](https://github.com/DreamMaoMao/mangowc)**. - with full workspace switching, overview integration, and monitor management.
|
||||
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
||||
|
||||
Other Wayland compositors work too, just with a reduced feature set.
|
||||
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
||||
|
||||
**[Compositor configuration guide →](https://danklinux.com/docs/dankmaterialshell/compositors)**
|
||||
## Command Line Interface
|
||||
|
||||
---
|
||||
|
||||
## Keybinds & IPC
|
||||
|
||||
Control everything from the command line or keybinds:
|
||||
Control the shell from the command line or keybinds:
|
||||
|
||||
```bash
|
||||
dms run # Start the shell
|
||||
dms ipc call spotlight toggle
|
||||
dms ipc call audio setvolume 50
|
||||
dms ipc call wallpaper set /path/to/image.jpg
|
||||
dms ipc call theme toggle
|
||||
dms brightness list # List available displays
|
||||
dms plugins search # Browse plugin registry
|
||||
```
|
||||
|
||||
**[Full keybind and IPC documentation →](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)**
|
||||
|
||||
---
|
||||
|
||||
## Theming
|
||||
|
||||
DankMaterialShell automatically generates color schemes from your wallpaper or theme and applies them to GTK, Qt, terminals, and more.
|
||||
|
||||
DMS is not opinionated or forcing these themes - they are created as optional themes you can enable. You can refer to the documentation if you want to use them:
|
||||
|
||||
**Application theming:** [GTK, Qt, Firefox, terminals, vscode+vscodium →](https://danklinux.com/docs/dankmaterialshell/application-themes)
|
||||
|
||||
**Custom themes:** [Create your own color schemes →](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
||||
|
||||
---
|
||||
|
||||
## Plugins
|
||||
|
||||
Extend dms with the plugin system. Browse community plugins at [plugins.danklinux.com](https://plugins.danklinux.com).
|
||||
|
||||
**[Plugin development guide →](https://danklinux.com/docs/dankmaterialshell/plugins-overview)**
|
||||
|
||||
---
|
||||
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
|
||||
|
||||
## Documentation
|
||||
|
||||
**Website:** [danklinux.com](https://danklinux.com)
|
||||
- **Website:** [danklinux.com](https://danklinux.com)
|
||||
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs/)
|
||||
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
||||
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
||||
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
||||
|
||||
**Docs:** [danklinux.com/docs](https://danklinux.com/docs)
|
||||
## Development
|
||||
|
||||
**Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
||||
See component-specific documentation:
|
||||
|
||||
---
|
||||
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
|
||||
- **[core/](core/)** - Go backend, CLI tools, and system integration
|
||||
- **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
|
||||
|
||||
### Building from Source
|
||||
|
||||
**Core + Dankinstall:**
|
||||
|
||||
```bash
|
||||
cd core
|
||||
make # Build dms CLI
|
||||
make dankinstall # Build installer
|
||||
```
|
||||
|
||||
**Shell:**
|
||||
|
||||
```bash
|
||||
quickshell -p quickshell/
|
||||
```
|
||||
|
||||
**NixOS:**
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||
|
||||
# Use in home-manager or NixOS configuration
|
||||
imports = [ inputs.dms.homeModules.dank-material-shell ];
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Bug fixes, new widgets, theme improvements, or docs - it all helps.
|
||||
Contributions welcome. Bug fixes, widgets, features, documentation, and plugins all help.
|
||||
|
||||
**Contributing Code:**
|
||||
1. Fork the repository
|
||||
2. Set up the development environment
|
||||
3. Make your changes
|
||||
2. Make your changes
|
||||
3. Test thoroughly
|
||||
4. Open a pull request
|
||||
|
||||
**Contributing Documentation:**
|
||||
1. Fork the [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs) repository
|
||||
2. Update files in the `docs/` folder
|
||||
3. Open a pull request
|
||||
|
||||
### Development Setup
|
||||
|
||||
**Requirements:**
|
||||
- `python3` - Translation management
|
||||
|
||||
**Git Hooks:**
|
||||
|
||||
Enable the pre-commit hook to check translation sync status:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
**Translation Workflow**
|
||||
|
||||
Set POEditor credentials:
|
||||
|
||||
```bash
|
||||
export POEDITOR_API_TOKEN="your_api_token"
|
||||
export POEDITOR_PROJECT_ID="your_project_id"
|
||||
```
|
||||
|
||||
Sync translations before committing:
|
||||
|
||||
```bash
|
||||
python3 scripts/i18nsync.py sync
|
||||
```
|
||||
|
||||
This script:
|
||||
- Extracts strings from QML files
|
||||
- Uploads changed English terms to POEditor
|
||||
- Downloads updated translations from POEditor
|
||||
- Stages all changes for commit
|
||||
|
||||
The pre-commit hook will block commits if translations are out of sync and remind you to run the sync script.
|
||||
|
||||
Without POEditor credentials, the hook is skipped and commits proceed normally.
|
||||
|
||||
Check the [issues](https://github.com/AvengeMedia/DankMaterialShell/issues) or join the community.
|
||||
|
||||
---
|
||||
For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs).
|
||||
|
||||
## Credits
|
||||
|
||||
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
|
||||
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
|
||||
- [Ly-sec](http://github.com/ly-sec) for awesome wallpaper effects among other things from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
||||
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
|
||||
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
|
||||
- [quickshell](https://quickshell.org/) - Shell framework
|
||||
- [niri](https://github.com/YaLTeR/niri) - Scrolling window manager
|
||||
- [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
||||
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
|
||||
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See [LICENSE](LICENSE) for details.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<int> values: Array(6)
|
||||
property int refCount: 0
|
||||
property bool cavaAvailable: false
|
||||
|
||||
Process {
|
||||
id: cavaCheck
|
||||
|
||||
command: ["which", "cava"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.cavaAvailable = exitCode === 0
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
cavaCheck.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProcess
|
||||
|
||||
running: root.cavaAvailable && root.refCount > 0
|
||||
command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`]
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
root.values = Array(6).fill(0)
|
||||
}
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
if (root.refCount > 0 && data.trim()) {
|
||||
let points = data.split(";").map(p => {
|
||||
return parseInt(p.trim(), 10)
|
||||
}).filter(p => {
|
||||
return !isNaN(p)
|
||||
})
|
||||
if (points.length >= 6) {
|
||||
root.values = points.slice(0, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,482 +0,0 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool isHyprland: false
|
||||
property bool isNiri: false
|
||||
property bool isDwl: false
|
||||
property bool isSway: false
|
||||
property bool isLabwc: false
|
||||
property string compositor: "unknown"
|
||||
|
||||
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
|
||||
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
|
||||
readonly property string swaySocket: Quickshell.env("SWAYSOCK")
|
||||
readonly property string labwcPid: Quickshell.env("LABWC_PID")
|
||||
property bool useNiriSorting: isNiri && NiriService
|
||||
|
||||
property var sortedToplevels: []
|
||||
property bool _sortScheduled: false
|
||||
|
||||
signal toplevelsChanged()
|
||||
|
||||
function getScreenScale(screen) {
|
||||
if (!screen) return 1
|
||||
|
||||
if (Quickshell.env("QT_WAYLAND_FORCE_DPI") || Quickshell.env("QT_SCALE_FACTOR")) {
|
||||
return screen.devicePixelRatio || 1
|
||||
}
|
||||
|
||||
if (WlrOutputService.wlrOutputAvailable && screen) {
|
||||
const wlrOutput = WlrOutputService.getOutput(screen.name)
|
||||
if (wlrOutput?.enabled && wlrOutput.scale !== undefined && wlrOutput.scale > 0) {
|
||||
return wlrOutput.scale
|
||||
}
|
||||
}
|
||||
|
||||
if (isNiri && screen) {
|
||||
const niriScale = NiriService.displayScales[screen.name]
|
||||
if (niriScale !== undefined) return niriScale
|
||||
}
|
||||
|
||||
if (isHyprland && screen) {
|
||||
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
|
||||
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
|
||||
}
|
||||
|
||||
if (isDwl && screen) {
|
||||
const dwlScale = DwlService.getOutputScale(screen.name)
|
||||
if (dwlScale !== undefined && dwlScale > 0) return dwlScale
|
||||
}
|
||||
|
||||
return screen?.devicePixelRatio || 1
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: sortDebounceTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
_sortScheduled = false
|
||||
sortedToplevels = computeSortedToplevels()
|
||||
toplevelsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSort() {
|
||||
if (_sortScheduled) return
|
||||
_sortScheduled = true
|
||||
sortDebounceTimer.restart()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ToplevelManager.toplevels
|
||||
function onValuesChanged() { root.scheduleSort() }
|
||||
}
|
||||
Connections {
|
||||
target: isHyprland ? Hyprland : null
|
||||
enabled: isHyprland
|
||||
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "openwindow" ||
|
||||
event.name === "closewindow" ||
|
||||
event.name === "movewindow" ||
|
||||
event.name === "movewindowv2" ||
|
||||
event.name === "workspace" ||
|
||||
event.name === "workspacev2" ||
|
||||
event.name === "focusedmon" ||
|
||||
event.name === "focusedmonv2" ||
|
||||
event.name === "activewindow" ||
|
||||
event.name === "activewindowv2" ||
|
||||
event.name === "changefloatingmode" ||
|
||||
event.name === "fullscreen" ||
|
||||
event.name === "moveintogroup" ||
|
||||
event.name === "moveoutofgroup") {
|
||||
try {
|
||||
Hyprland.refreshToplevels()
|
||||
} catch(e) {}
|
||||
root.scheduleSort()
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onWindowsChanged() { root.scheduleSort() }
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
detectCompositor()
|
||||
scheduleSort()
|
||||
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DwlService
|
||||
function onStateChanged() {
|
||||
if (isDwl && !isHyprland && !isNiri) {
|
||||
scheduleSort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function computeSortedToplevels() {
|
||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
|
||||
return []
|
||||
|
||||
if (useNiriSorting)
|
||||
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
|
||||
|
||||
if (isHyprland)
|
||||
return sortHyprlandToplevelsSafe()
|
||||
|
||||
return Array.from(ToplevelManager.toplevels.values)
|
||||
}
|
||||
|
||||
function _get(o, path, fallback) {
|
||||
try {
|
||||
let v = o
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
if (v === null || v === undefined) return fallback
|
||||
v = v[path[i]]
|
||||
}
|
||||
return (v === undefined || v === null) ? fallback : v
|
||||
} catch (e) { return fallback }
|
||||
}
|
||||
|
||||
function sortHyprlandToplevelsSafe() {
|
||||
if (!Hyprland.toplevels || !Hyprland.toplevels.values) return []
|
||||
|
||||
const items = Array.from(Hyprland.toplevels.values)
|
||||
|
||||
function _get(o, path, fb) {
|
||||
try {
|
||||
let v = o
|
||||
for (let k of path) { if (v == null) return fb; v = v[k] }
|
||||
return (v == null) ? fb : v
|
||||
} catch(e) { return fb }
|
||||
}
|
||||
|
||||
let snap = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const t = items[i]
|
||||
if (!t) continue
|
||||
|
||||
const addr = t.address || ""
|
||||
if (!addr) continue
|
||||
|
||||
const li = t.lastIpcObject || null
|
||||
|
||||
const monName = _get(li, ["monitor"], null) ?? _get(t, ["monitor", "name"], "")
|
||||
const monX = _get(t, ["monitor", "x"], Number.MAX_SAFE_INTEGER)
|
||||
const monY = _get(t, ["monitor", "y"], Number.MAX_SAFE_INTEGER)
|
||||
|
||||
const wsId = _get(li, ["workspace", "id"], null) ?? _get(t, ["workspace", "id"], Number.MAX_SAFE_INTEGER)
|
||||
|
||||
const at = _get(li, ["at"], null)
|
||||
let atX = (at !== null && at !== undefined && typeof at[0] === "number") ? at[0] : 1e9
|
||||
let atY = (at !== null && at !== undefined && typeof at[1] === "number") ? at[1] : 1e9
|
||||
|
||||
const relX = Number.isFinite(monX) ? (atX - monX) : atX
|
||||
const relY = Number.isFinite(monY) ? (atY - monY) : atY
|
||||
|
||||
snap.push({
|
||||
monKey: String(monName),
|
||||
monOrderX: Number.isFinite(monX) ? monX : Number.MAX_SAFE_INTEGER,
|
||||
monOrderY: Number.isFinite(monY) ? monY : Number.MAX_SAFE_INTEGER,
|
||||
wsId: (typeof wsId === "number") ? wsId : Number.MAX_SAFE_INTEGER,
|
||||
x: relX,
|
||||
y: relY,
|
||||
title: t.title || "",
|
||||
address: addr,
|
||||
wayland: t.wayland
|
||||
})
|
||||
}
|
||||
|
||||
const groups = new Map()
|
||||
for (const it of snap) {
|
||||
const key = it.monKey + "::" + it.wsId
|
||||
if (!groups.has(key)) groups.set(key, [])
|
||||
groups.get(key).push(it)
|
||||
}
|
||||
|
||||
let groupList = []
|
||||
for (const [key, arr] of groups) {
|
||||
const repr = arr[0]
|
||||
groupList.push({
|
||||
key,
|
||||
monKey: repr.monKey,
|
||||
monOrderX: repr.monOrderX,
|
||||
monOrderY: repr.monOrderY,
|
||||
wsId: repr.wsId,
|
||||
items: arr
|
||||
})
|
||||
}
|
||||
|
||||
groupList.sort((a, b) => {
|
||||
if (a.monOrderX !== b.monOrderX) return a.monOrderX - b.monOrderX
|
||||
if (a.monOrderY !== b.monOrderY) return a.monOrderY - b.monOrderY
|
||||
if (a.monKey !== b.monKey) return a.monKey.localeCompare(b.monKey)
|
||||
if (a.wsId !== b.wsId) return a.wsId - b.wsId
|
||||
return 0
|
||||
})
|
||||
|
||||
const COLUMN_THRESHOLD = 48
|
||||
const JITTER_Y = 6
|
||||
|
||||
let ordered = []
|
||||
for (const g of groupList) {
|
||||
const arr = g.items
|
||||
|
||||
const xs = arr.map(it => it.x).filter(x => Number.isFinite(x)).sort((a, b) => a - b)
|
||||
let colCenters = []
|
||||
if (xs.length > 0) {
|
||||
for (const x of xs) {
|
||||
if (colCenters.length === 0) {
|
||||
colCenters.push(x)
|
||||
} else {
|
||||
const last = colCenters[colCenters.length - 1]
|
||||
if (x - last >= COLUMN_THRESHOLD) {
|
||||
colCenters.push(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
colCenters = [0]
|
||||
}
|
||||
|
||||
for (const it of arr) {
|
||||
let bestCol = 0
|
||||
let bestDist = Number.POSITIVE_INFINITY
|
||||
for (let ci = 0; ci < colCenters.length; ci++) {
|
||||
const d = Math.abs(it.x - colCenters[ci])
|
||||
if (d < bestDist) {
|
||||
bestDist = d
|
||||
bestCol = ci
|
||||
}
|
||||
}
|
||||
it._col = bestCol
|
||||
}
|
||||
|
||||
arr.sort((a, b) => {
|
||||
if (a._col !== b._col) return a._col - b._col
|
||||
|
||||
const dy = a.y - b.y
|
||||
if (Math.abs(dy) > JITTER_Y) return dy
|
||||
|
||||
if (a.title !== b.title) return a.title.localeCompare(b.title)
|
||||
if (a.address !== b.address) return a.address.localeCompare(b.address)
|
||||
return 0
|
||||
})
|
||||
|
||||
ordered.push.apply(ordered, arr)
|
||||
}
|
||||
|
||||
return ordered.map(x => x.wayland).filter(w => w !== null && w !== undefined)
|
||||
}
|
||||
|
||||
function filterCurrentWorkspace(toplevels, screen) {
|
||||
if (useNiriSorting) return NiriService.filterCurrentWorkspace(toplevels, screen)
|
||||
if (isHyprland) return filterHyprlandCurrentWorkspaceSafe(toplevels, screen)
|
||||
return toplevels
|
||||
}
|
||||
|
||||
function filterHyprlandCurrentWorkspaceSafe(toplevels, screenName) {
|
||||
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) return toplevels
|
||||
|
||||
let currentWorkspaceId = null
|
||||
try {
|
||||
const hy = Array.from(Hyprland.toplevels.values)
|
||||
for (const t of hy) {
|
||||
const mon = _get(t, ["monitor", "name"], "")
|
||||
const wsId = _get(t, ["workspace", "id"], null)
|
||||
const active = !!_get(t, ["activated"], false)
|
||||
if (mon === screenName && wsId !== null) {
|
||||
if (active) { currentWorkspaceId = wsId; break }
|
||||
if (currentWorkspaceId === null) currentWorkspaceId = wsId
|
||||
}
|
||||
}
|
||||
|
||||
if (currentWorkspaceId === null && Hyprland.workspaces) {
|
||||
const wss = Array.from(Hyprland.workspaces.values)
|
||||
const focusedId = _get(Hyprland, ["focusedWorkspace", "id"], null)
|
||||
for (const ws of wss) {
|
||||
const monName = _get(ws, ["monitor"], "")
|
||||
const wsId = _get(ws, ["id"], null)
|
||||
if (monName === screenName && wsId !== null) {
|
||||
if (focusedId !== null && wsId === focusedId) { currentWorkspaceId = wsId; break }
|
||||
if (currentWorkspaceId === null) currentWorkspaceId = wsId
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("CompositorService: workspace snapshot failed:", e)
|
||||
}
|
||||
|
||||
if (currentWorkspaceId === null) return toplevels
|
||||
|
||||
// Map wayland → wsId snapshot
|
||||
let map = new Map()
|
||||
try {
|
||||
const hy = Array.from(Hyprland.toplevels.values)
|
||||
for (const t of hy) {
|
||||
const wsId = _get(t, ["workspace", "id"], null)
|
||||
if (t && t.wayland && wsId !== null) map.set(t.wayland, wsId)
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return toplevels.filter(w => map.get(w) === currentWorkspaceId)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: compositorInitTimer
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
detectCompositor()
|
||||
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
|
||||
}
|
||||
}
|
||||
|
||||
function detectCompositor() {
|
||||
if (hyprlandSignature && hyprlandSignature.length > 0 &&
|
||||
!niriSocket && !swaySocket && !labwcPid) {
|
||||
isHyprland = true
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "hyprland"
|
||||
console.info("CompositorService: Detected Hyprland")
|
||||
return
|
||||
}
|
||||
|
||||
if (niriSocket && niriSocket.length > 0) {
|
||||
Proc.runCommand("niriSocketCheck", ["test", "-S", niriSocket], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
isNiri = true
|
||||
isHyprland = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "niri"
|
||||
console.info("CompositorService: Detected Niri with socket:", niriSocket)
|
||||
NiriService.generateNiriBinds()
|
||||
NiriService.generateNiriBlurrule()
|
||||
}
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (swaySocket && swaySocket.length > 0) {
|
||||
Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
isNiri = false
|
||||
isHyprland = false
|
||||
isDwl = false
|
||||
isSway = true
|
||||
isLabwc = false
|
||||
compositor = "sway"
|
||||
console.info("CompositorService: Detected Sway with socket:", swaySocket)
|
||||
}
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (labwcPid && labwcPid.length > 0) {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = true
|
||||
compositor = "labwc"
|
||||
console.info("CompositorService: Detected LabWC with PID:", labwcPid)
|
||||
return
|
||||
}
|
||||
|
||||
if (DMSService.dmsAvailable) {
|
||||
Qt.callLater(checkForDwl)
|
||||
} else {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "unknown"
|
||||
console.warn("CompositorService: No compositor detected")
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
function onCapabilitiesReceived() {
|
||||
if (!isHyprland && !isNiri && !isDwl && !isLabwc) {
|
||||
checkForDwl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkForDwl() {
|
||||
if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
isDwl = true
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "dwl"
|
||||
console.info("CompositorService: Detected DWL via DMS capability")
|
||||
}
|
||||
}
|
||||
|
||||
function powerOffMonitors() {
|
||||
if (isNiri) return NiriService.powerOffMonitors()
|
||||
if (isHyprland) return Hyprland.dispatch("dpms off")
|
||||
if (isDwl) return _dwlPowerOffMonitors()
|
||||
if (isSway) { try { I3.dispatch("output * dpms off") } catch(_){} return }
|
||||
console.warn("CompositorService: Cannot power off monitors, unknown compositor")
|
||||
}
|
||||
|
||||
function powerOnMonitors() {
|
||||
if (isNiri) return NiriService.powerOnMonitors()
|
||||
if (isHyprland) return Hyprland.dispatch("dpms on")
|
||||
if (isDwl) return _dwlPowerOnMonitors()
|
||||
if (isSway) { try { I3.dispatch("output * dpms on") } catch(_){} return }
|
||||
console.warn("CompositorService: Cannot power on monitors, unknown compositor")
|
||||
}
|
||||
|
||||
function _dwlPowerOffMonitors() {
|
||||
if (!Quickshell.screens || Quickshell.screens.length === 0) {
|
||||
console.warn("CompositorService: No screens available for DWL power off")
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
const screen = Quickshell.screens[i]
|
||||
if (screen && screen.name) {
|
||||
Quickshell.execDetached(["mmsg", "-d", "disable_monitor," + screen.name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _dwlPowerOnMonitors() {
|
||||
if (!Quickshell.screens || Quickshell.screens.length === 0) {
|
||||
console.warn("CompositorService: No screens available for DWL power on")
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
const screen = Quickshell.screens[i]
|
||||
if (screen && screen.name) {
|
||||
Quickshell.execDetached(["mmsg", "-d", "enable_monitor," + screen.name])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int refCount: 0
|
||||
|
||||
property var printerNames: []
|
||||
property var printers: []
|
||||
property string selectedPrinter: ""
|
||||
|
||||
property bool cupsAvailable: false
|
||||
property bool stateInitialized: false
|
||||
|
||||
signal cupsStateUpdate
|
||||
|
||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||
|
||||
Component.onCompleted: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
checkDMSCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected) {
|
||||
checkDMSCapabilities()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
enabled: DMSService.isConnected
|
||||
|
||||
function onCupsStateUpdate(data) {
|
||||
console.log("CupsService: Subscription update received")
|
||||
getState()
|
||||
}
|
||||
|
||||
function onCapabilitiesChanged() {
|
||||
checkDMSCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
function checkDMSCapabilities() {
|
||||
if (!DMSService.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (DMSService.capabilities.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
cupsAvailable = DMSService.capabilities.includes("cups")
|
||||
|
||||
if (cupsAvailable && !stateInitialized) {
|
||||
stateInitialized = true
|
||||
getState()
|
||||
}
|
||||
}
|
||||
|
||||
function getState() {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
DMSService.sendRequest("cups.getPrinters", null, response => {
|
||||
if (response.result) {
|
||||
updatePrinters(response.result)
|
||||
fetchAllJobs()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updatePrinters(printersData) {
|
||||
printerNames = printersData.map(p => p.name)
|
||||
|
||||
let printersObj = {}
|
||||
for (var i = 0; i < printersData.length; i++) {
|
||||
let printer = printersData[i]
|
||||
printersObj[printer.name] = {
|
||||
"state": printer.state,
|
||||
"stateReason": printer.stateReason,
|
||||
"jobs": []
|
||||
}
|
||||
}
|
||||
printers = printersObj
|
||||
|
||||
if (printerNames.length > 0) {
|
||||
if (selectedPrinter.length > 0) {
|
||||
if (!printerNames.includes(selectedPrinter)) {
|
||||
selectedPrinter = printerNames[0]
|
||||
}
|
||||
} else {
|
||||
selectedPrinter = printerNames[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAllJobs() {
|
||||
for (var i = 0; i < printerNames.length; i++) {
|
||||
fetchJobsForPrinter(printerNames[i])
|
||||
}
|
||||
}
|
||||
|
||||
function fetchJobsForPrinter(printerName) {
|
||||
const params = {
|
||||
"printerName": printerName
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.getJobs", params, response => {
|
||||
if (response.result && printers[printerName]) {
|
||||
let updatedPrinters = Object.assign({}, printers)
|
||||
updatedPrinters[printerName].jobs = response.result
|
||||
printers = updatedPrinters
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getSelectedPrinter() {
|
||||
return selectedPrinter
|
||||
}
|
||||
|
||||
function setSelectedPrinter(printerName) {
|
||||
if (printerNames.length > 0) {
|
||||
if (printerNames.includes(printerName)) {
|
||||
selectedPrinter = printerName
|
||||
} else {
|
||||
selectedPrinter = printerNames[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPrintersNum() {
|
||||
if (!cupsAvailable)
|
||||
return 0
|
||||
|
||||
return printerNames.length
|
||||
}
|
||||
|
||||
function getPrintersNames() {
|
||||
if (!cupsAvailable)
|
||||
return []
|
||||
|
||||
return printerNames
|
||||
}
|
||||
|
||||
function getTotalJobsNum() {
|
||||
if (!cupsAvailable)
|
||||
return 0
|
||||
|
||||
var result = 0
|
||||
for (var i = 0; i < printerNames.length; i++) {
|
||||
var printerName = printerNames[i]
|
||||
if (printers[printerName] && printers[printerName].jobs) {
|
||||
result += printers[printerName].jobs.length
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getCurrentPrinterState() {
|
||||
if (!cupsAvailable || !selectedPrinter)
|
||||
return ""
|
||||
|
||||
var printer = printers[selectedPrinter]
|
||||
return printer.state
|
||||
}
|
||||
|
||||
function getCurrentPrinterStatePrettyShort() {
|
||||
if (!cupsAvailable || !selectedPrinter)
|
||||
return ""
|
||||
|
||||
var printer = printers[selectedPrinter]
|
||||
return getPrinterStateTranslation(printer.state) + " (" + getPrinterStateReasonTranslation(printer.stateReason) + ")"
|
||||
}
|
||||
|
||||
function getCurrentPrinterStatePretty() {
|
||||
if (!cupsAvailable || !selectedPrinter)
|
||||
return ""
|
||||
|
||||
var printer = printers[selectedPrinter]
|
||||
return getPrinterStateTranslation(printer.state) + " (" + I18n.tr("Reason") + ": " + getPrinterStateReasonTranslation(printer.stateReason) + ")"
|
||||
}
|
||||
|
||||
function getCurrentPrinterJobs() {
|
||||
if (!cupsAvailable || !selectedPrinter)
|
||||
return []
|
||||
|
||||
return getJobs(selectedPrinter)
|
||||
}
|
||||
|
||||
function getJobs(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return ""
|
||||
|
||||
var printer = printers[printerName]
|
||||
return printer.jobs
|
||||
}
|
||||
|
||||
function getJobsNum(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return 0
|
||||
|
||||
var printer = printers[printerName]
|
||||
return printer.jobs.length
|
||||
}
|
||||
|
||||
function pausePrinter(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
const params = {
|
||||
"printerName": printerName
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.pausePrinter", params, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to pause printer") + " - " + response.error)
|
||||
} else {
|
||||
getState()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resumePrinter(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
const params = {
|
||||
"printerName": printerName
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.resumePrinter", params, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to resume printer") + " - " + response.error)
|
||||
} else {
|
||||
getState()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function cancelJob(printerName, jobID) {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
const params = {
|
||||
"printerName": printerName,
|
||||
"jobID": jobID
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.cancelJob", params, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to cancel selected job") + " - " + response.error)
|
||||
} else {
|
||||
fetchJobsForPrinter(printerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function purgeJobs(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
const params = {
|
||||
"printerName": printerName
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.purgeJobs", params, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to cancel all jobs") + " - " + response.error)
|
||||
} else {
|
||||
fetchJobsForPrinter(printerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
readonly property var states: ({
|
||||
"idle": I18n.tr("Idle"),
|
||||
"processing": I18n.tr("Processing"),
|
||||
"stopped": I18n.tr("Stopped")
|
||||
})
|
||||
|
||||
readonly property var reasonsGeneral: ({
|
||||
"none": I18n.tr("None"),
|
||||
"other": I18n.tr("Other")
|
||||
})
|
||||
|
||||
readonly property var reasonsSupplies: ({
|
||||
"toner-low": I18n.tr("Toner Low"),
|
||||
"toner-empty": I18n.tr("Toner Empty"),
|
||||
"marker-supply-low": I18n.tr("Marker Supply Low"),
|
||||
"marker-supply-empty": I18n.tr("Marker Supply Empty"),
|
||||
"marker-waste-almost-full": I18n.tr("Marker Waste Almost Full"),
|
||||
"marker-waste-full": I18n.tr("Marker Waste Full")
|
||||
})
|
||||
|
||||
readonly property var reasonsMedia: ({
|
||||
"media-low": I18n.tr("Media Low"),
|
||||
"media-empty": I18n.tr("Media Empty"),
|
||||
"media-needed": I18n.tr("Media Needed"),
|
||||
"media-jam": I18n.tr("Media Jam")
|
||||
})
|
||||
|
||||
readonly property var reasonsParts: ({
|
||||
"cover-open": I18n.tr("Cover Open"),
|
||||
"door-open": I18n.tr("Door Open"),
|
||||
"interlock-open": I18n.tr("Interlock Open"),
|
||||
"output-tray-missing": I18n.tr("Output Tray Missing"),
|
||||
"output-area-almost-full": I18n.tr("Output Area Almost Full"),
|
||||
"output-area-full": I18n.tr("Output Area Full")
|
||||
})
|
||||
|
||||
readonly property var reasonsErrors: ({
|
||||
"paused": I18n.tr("Paused"),
|
||||
"shutdown": I18n.tr("Shutdown"),
|
||||
"connecting-to-device": I18n.tr("Connecting to Device"),
|
||||
"timed-out": I18n.tr("Timed Out"),
|
||||
"stopping": I18n.tr("Stopping"),
|
||||
"stopped-partly": I18n.tr("Stopped Partly")
|
||||
})
|
||||
|
||||
readonly property var reasonsService: ({
|
||||
"spool-area-full": I18n.tr("Spool Area Full"),
|
||||
"cups-missing-filter-warning": I18n.tr("CUPS Missing Filter Warning"),
|
||||
"cups-insecure-filter-warning": I18n.tr("CUPS Insecure Filter Warning")
|
||||
})
|
||||
|
||||
readonly property var reasonsConnectivity: ({
|
||||
"offline-report": I18n.tr("Offline Report"),
|
||||
"moving-to-paused": I18n.tr("Moving to Paused")
|
||||
})
|
||||
|
||||
readonly property var severitySuffixes: ({
|
||||
"-error": I18n.tr("Error"),
|
||||
"-warning": I18n.tr("Warning"),
|
||||
"-report": I18n.tr("Report")
|
||||
})
|
||||
|
||||
function getPrinterStateTranslation(state) {
|
||||
return states[state] || state
|
||||
}
|
||||
|
||||
function getPrinterStateReasonTranslation(reason) {
|
||||
let allReasons = Object.assign({}, reasonsGeneral, reasonsSupplies, reasonsMedia, reasonsParts, reasonsErrors, reasonsService, reasonsConnectivity)
|
||||
|
||||
let basReason = reason
|
||||
let suffix = ""
|
||||
|
||||
for (let s in severitySuffixes) {
|
||||
if (reason.endsWith(s)) {
|
||||
basReason = reason.slice(0, -s.length)
|
||||
suffix = severitySuffixes[s]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let translation = allReasons[basReason] || basReason
|
||||
return suffix ? translation + " (" + suffix + ")" : translation
|
||||
}
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool dmsAvailable: false
|
||||
property var capabilities: []
|
||||
property int apiVersion: 0
|
||||
readonly property int expectedApiVersion: 1
|
||||
property var availablePlugins: []
|
||||
property var installedPlugins: []
|
||||
property bool isConnected: false
|
||||
property bool isConnecting: false
|
||||
property bool subscribeConnected: false
|
||||
readonly property bool forceExtWorkspace: false
|
||||
|
||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||
|
||||
property var pendingRequests: ({})
|
||||
property int requestIdCounter: 0
|
||||
property bool shownOutdatedError: false
|
||||
property string updateCommand: "dms update"
|
||||
property bool checkingUpdateCommand: false
|
||||
|
||||
signal pluginsListReceived(var plugins)
|
||||
signal installedPluginsReceived(var plugins)
|
||||
signal searchResultsReceived(var plugins)
|
||||
signal operationSuccess(string message)
|
||||
signal operationError(string error)
|
||||
signal connectionStateChanged
|
||||
|
||||
signal networkStateUpdate(var data)
|
||||
signal cupsStateUpdate(var data)
|
||||
signal loginctlStateUpdate(var data)
|
||||
signal loginctlEvent(var event)
|
||||
signal capabilitiesReceived
|
||||
signal credentialsRequest(var data)
|
||||
signal bluetoothPairingRequest(var data)
|
||||
signal dwlStateUpdate(var data)
|
||||
signal brightnessStateUpdate(var data)
|
||||
signal brightnessDeviceUpdate(var device)
|
||||
signal extWorkspaceStateUpdate(var data)
|
||||
signal wlrOutputStateUpdate(var data)
|
||||
|
||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput"]
|
||||
|
||||
Component.onCompleted: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
detectUpdateCommand()
|
||||
}
|
||||
}
|
||||
|
||||
function detectUpdateCommand() {
|
||||
checkingUpdateCommand = true
|
||||
checkAurHelper.running = true
|
||||
}
|
||||
|
||||
function startSocketConnection() {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
testProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: checkAurHelper
|
||||
command: ["sh", "-c", "command -v paru || command -v yay"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const helper = text.trim()
|
||||
if (helper.includes("paru")) {
|
||||
checkDmsPackage.helper = "paru"
|
||||
checkDmsPackage.running = true
|
||||
} else if (helper.includes("yay")) {
|
||||
checkDmsPackage.helper = "yay"
|
||||
checkDmsPackage.running = true
|
||||
} else {
|
||||
updateCommand = "dms update"
|
||||
checkingUpdateCommand = false
|
||||
startSocketConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
updateCommand = "dms update"
|
||||
checkingUpdateCommand = false
|
||||
startSocketConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: checkDmsPackage
|
||||
property string helper: ""
|
||||
command: ["sh", "-c", "pacman -Qi dms-shell-git 2>/dev/null || pacman -Qi dms-shell-bin 2>/dev/null"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.includes("dms-shell-git")) {
|
||||
updateCommand = checkDmsPackage.helper + " -S dms-shell-git"
|
||||
} else if (text.includes("dms-shell-bin")) {
|
||||
updateCommand = checkDmsPackage.helper + " -S dms-shell-bin"
|
||||
} else {
|
||||
updateCommand = "dms update"
|
||||
}
|
||||
checkingUpdateCommand = false
|
||||
startSocketConnection()
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
updateCommand = "dms update"
|
||||
checkingUpdateCommand = false
|
||||
startSocketConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: testProcess
|
||||
command: ["test", "-S", root.socketPath]
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
root.dmsAvailable = true
|
||||
connectSocket()
|
||||
} else {
|
||||
root.dmsAvailable = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function connectSocket() {
|
||||
if (!dmsAvailable || isConnected || isConnecting) {
|
||||
return
|
||||
}
|
||||
|
||||
isConnecting = true
|
||||
requestSocket.connected = true
|
||||
}
|
||||
|
||||
DankSocket {
|
||||
id: requestSocket
|
||||
path: root.socketPath
|
||||
connected: false
|
||||
|
||||
onConnectionStateChanged: {
|
||||
if (connected) {
|
||||
root.isConnected = true
|
||||
root.isConnecting = false
|
||||
root.connectionStateChanged()
|
||||
subscribeSocket.connected = true
|
||||
} else {
|
||||
root.isConnected = false
|
||||
root.isConnecting = false
|
||||
root.apiVersion = 0
|
||||
root.capabilities = []
|
||||
root.connectionStateChanged()
|
||||
}
|
||||
}
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => {
|
||||
if (!line || line.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("DMSService: Request socket <<", line)
|
||||
|
||||
try {
|
||||
const response = JSON.parse(line)
|
||||
handleResponse(response)
|
||||
} catch (e) {
|
||||
console.warn("DMSService: Failed to parse request response:", line, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankSocket {
|
||||
id: subscribeSocket
|
||||
path: root.socketPath
|
||||
connected: false
|
||||
|
||||
onConnectionStateChanged: {
|
||||
root.subscribeConnected = connected
|
||||
if (connected) {
|
||||
sendSubscribeRequest()
|
||||
}
|
||||
}
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => {
|
||||
if (!line || line.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("DMSService: Subscribe socket <<", line)
|
||||
|
||||
try {
|
||||
const response = JSON.parse(line)
|
||||
handleSubscriptionEvent(response)
|
||||
} catch (e) {
|
||||
console.warn("DMSService: Failed to parse subscription event:", line, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendSubscribeRequest() {
|
||||
const request = {
|
||||
"method": "subscribe"
|
||||
}
|
||||
|
||||
if (activeSubscriptions.length > 0) {
|
||||
request.params = {
|
||||
"services": activeSubscriptions
|
||||
}
|
||||
console.log("DMSService: Subscribing to services:", JSON.stringify(activeSubscriptions))
|
||||
} else {
|
||||
console.log("DMSService: Subscribing to all services")
|
||||
}
|
||||
|
||||
subscribeSocket.send(request)
|
||||
}
|
||||
|
||||
function subscribe(services) {
|
||||
if (!Array.isArray(services)) {
|
||||
services = [services]
|
||||
}
|
||||
|
||||
activeSubscriptions = services
|
||||
|
||||
if (subscribeConnected) {
|
||||
subscribeSocket.connected = false
|
||||
Qt.callLater(() => {
|
||||
subscribeSocket.connected = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addSubscription(service) {
|
||||
if (activeSubscriptions.includes("all")) {
|
||||
console.warn("DMSService: Cannot add specific subscription when subscribed to 'all'")
|
||||
return
|
||||
}
|
||||
|
||||
if (!activeSubscriptions.includes(service)) {
|
||||
const newSubs = [...activeSubscriptions, service]
|
||||
subscribe(newSubs)
|
||||
}
|
||||
}
|
||||
|
||||
function removeSubscription(service) {
|
||||
if (activeSubscriptions.includes("all")) {
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace"]
|
||||
const filtered = allServices.filter(s => s !== service)
|
||||
subscribe(filtered)
|
||||
} else {
|
||||
const filtered = activeSubscriptions.filter(s => s !== service)
|
||||
if (filtered.length === 0) {
|
||||
console.warn("DMSService: Cannot remove last subscription")
|
||||
return
|
||||
}
|
||||
subscribe(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeAll() {
|
||||
subscribe(["all"])
|
||||
}
|
||||
|
||||
function subscribeAllExcept(excludeServices) {
|
||||
if (!Array.isArray(excludeServices)) {
|
||||
excludeServices = [excludeServices]
|
||||
}
|
||||
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace"]
|
||||
const filtered = allServices.filter(s => !excludeServices.includes(s))
|
||||
subscribe(filtered)
|
||||
}
|
||||
|
||||
function handleSubscriptionEvent(response) {
|
||||
if (response.error) {
|
||||
if (response.error.includes("unknown method") && response.error.includes("subscribe")) {
|
||||
if (!shownOutdatedError) {
|
||||
console.error("DMSService: Server does not support subscribe method")
|
||||
ToastService.showError(I18n.tr("DMS out of date"), I18n.tr("To update, run the following command:"), updateCommand)
|
||||
shownOutdatedError = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!response.result) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = response.result.service
|
||||
const data = response.result.data
|
||||
|
||||
if (service === "server") {
|
||||
apiVersion = data.apiVersion || 0
|
||||
capabilities = data.capabilities || []
|
||||
|
||||
console.info("DMSService: Connected (API v" + apiVersion + ") -", JSON.stringify(capabilities))
|
||||
|
||||
if (apiVersion < expectedApiVersion) {
|
||||
ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")")
|
||||
}
|
||||
|
||||
capabilitiesReceived()
|
||||
} else if (service === "network") {
|
||||
networkStateUpdate(data)
|
||||
} else if (service === "network.credentials") {
|
||||
credentialsRequest(data)
|
||||
} else if (service === "loginctl") {
|
||||
if (data.event) {
|
||||
loginctlEvent(data)
|
||||
} else {
|
||||
loginctlStateUpdate(data)
|
||||
}
|
||||
} else if (service === "bluetooth.pairing") {
|
||||
bluetoothPairingRequest(data)
|
||||
} else if (service === "cups") {
|
||||
cupsStateUpdate(data)
|
||||
} else if (service === "dwl") {
|
||||
dwlStateUpdate(data)
|
||||
} else if (service === "brightness") {
|
||||
brightnessStateUpdate(data)
|
||||
} else if (service === "brightness.update") {
|
||||
if (data.device) {
|
||||
brightnessDeviceUpdate(data.device)
|
||||
}
|
||||
} else if (service === "extworkspace") {
|
||||
extWorkspaceStateUpdate(data)
|
||||
} else if (service === "wlroutput") {
|
||||
wlrOutputStateUpdate(data)
|
||||
}
|
||||
}
|
||||
|
||||
function sendRequest(method, params, callback) {
|
||||
if (!isConnected) {
|
||||
console.warn("DMSService.sendRequest: Not connected, method:", method)
|
||||
if (callback) {
|
||||
callback({
|
||||
"error": "not connected to DMS socket"
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
requestIdCounter++
|
||||
const id = Date.now() + requestIdCounter
|
||||
const request = {
|
||||
"id": id,
|
||||
"method": method
|
||||
}
|
||||
|
||||
if (params) {
|
||||
request.params = params
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
pendingRequests[id] = callback
|
||||
}
|
||||
|
||||
console.log("DMSService.sendRequest: Sending request id=" + id + " method=" + method)
|
||||
requestSocket.send(request)
|
||||
}
|
||||
|
||||
function handleResponse(response) {
|
||||
const callback = pendingRequests[response.id]
|
||||
|
||||
if (callback) {
|
||||
delete pendingRequests[response.id]
|
||||
callback(response)
|
||||
}
|
||||
}
|
||||
|
||||
function ping(callback) {
|
||||
sendRequest("ping", null, callback)
|
||||
}
|
||||
|
||||
function listPlugins(callback) {
|
||||
sendRequest("plugins.list", null, response => {
|
||||
if (response.result) {
|
||||
availablePlugins = response.result
|
||||
pluginsListReceived(response.result)
|
||||
}
|
||||
if (callback) {
|
||||
callback(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function listInstalled(callback) {
|
||||
sendRequest("plugins.listInstalled", null, response => {
|
||||
if (response.result) {
|
||||
installedPlugins = response.result
|
||||
installedPluginsReceived(response.result)
|
||||
}
|
||||
if (callback) {
|
||||
callback(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function search(query, category, compositor, capability, callback) {
|
||||
const params = {
|
||||
"query": query
|
||||
}
|
||||
if (category) {
|
||||
params.category = category
|
||||
}
|
||||
if (compositor) {
|
||||
params.compositor = compositor
|
||||
}
|
||||
if (capability) {
|
||||
params.capability = capability
|
||||
}
|
||||
|
||||
sendRequest("plugins.search", params, response => {
|
||||
if (response.result) {
|
||||
searchResultsReceived(response.result)
|
||||
}
|
||||
if (callback) {
|
||||
callback(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function install(pluginName, callback) {
|
||||
sendRequest("plugins.install", {
|
||||
"name": pluginName
|
||||
}, response => {
|
||||
if (callback) {
|
||||
callback(response)
|
||||
}
|
||||
if (!response.error) {
|
||||
listInstalled()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function uninstall(pluginName, callback) {
|
||||
sendRequest("plugins.uninstall", {
|
||||
"name": pluginName
|
||||
}, response => {
|
||||
if (callback) {
|
||||
callback(response)
|
||||
}
|
||||
if (!response.error) {
|
||||
listInstalled()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function update(pluginName, callback) {
|
||||
sendRequest("plugins.update", {
|
||||
"name": pluginName
|
||||
}, response => {
|
||||
if (callback) {
|
||||
callback(response)
|
||||
}
|
||||
if (!response.error) {
|
||||
listInstalled()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function lockSession(callback) {
|
||||
sendRequest("loginctl.lock", null, callback)
|
||||
}
|
||||
|
||||
function unlockSession(callback) {
|
||||
sendRequest("loginctl.unlock", null, callback)
|
||||
}
|
||||
|
||||
function bluetoothPair(devicePath, callback) {
|
||||
sendRequest("bluetooth.pair", {
|
||||
"device": devicePath
|
||||
}, callback)
|
||||
}
|
||||
|
||||
function bluetoothConnect(devicePath, callback) {
|
||||
sendRequest("bluetooth.connect", {
|
||||
"device": devicePath
|
||||
}, callback)
|
||||
}
|
||||
|
||||
function bluetoothDisconnect(devicePath, callback) {
|
||||
sendRequest("bluetooth.disconnect", {
|
||||
"device": devicePath
|
||||
}, callback)
|
||||
}
|
||||
|
||||
function bluetoothRemove(devicePath, callback) {
|
||||
sendRequest("bluetooth.remove", {
|
||||
"device": devicePath
|
||||
}, callback)
|
||||
}
|
||||
|
||||
function bluetoothTrust(devicePath, callback) {
|
||||
sendRequest("bluetooth.trust", {
|
||||
"device": devicePath
|
||||
}, callback)
|
||||
}
|
||||
|
||||
function bluetoothSubmitPairing(token, secrets, accept, callback) {
|
||||
sendRequest("bluetooth.pairing.submit", {
|
||||
"token": token,
|
||||
"secrets": secrets,
|
||||
"accept": accept
|
||||
}, callback)
|
||||
}
|
||||
|
||||
function bluetoothCancelPairing(token, callback) {
|
||||
sendRequest("bluetooth.pairing.cancel", {
|
||||
"token": token
|
||||
}, callback)
|
||||
}
|
||||
}
|
||||
@@ -1,936 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool brightnessAvailable: devices.length > 0
|
||||
property var devices: []
|
||||
property var deviceBrightness: ({})
|
||||
property var deviceBrightnessUserSet: ({})
|
||||
property var deviceMaxCache: ({})
|
||||
property int brightnessVersion: 0
|
||||
property string currentDevice: ""
|
||||
property string lastIpcDevice: ""
|
||||
property int brightnessLevel: {
|
||||
brightnessVersion
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
if (!deviceToUse) {
|
||||
return 50
|
||||
}
|
||||
|
||||
return getDeviceBrightness(deviceToUse)
|
||||
}
|
||||
property int maxBrightness: 100
|
||||
property bool brightnessInitialized: false
|
||||
|
||||
signal brightnessChanged(bool showOsd)
|
||||
signal deviceSwitched
|
||||
|
||||
property bool nightModeActive: nightModeEnabled
|
||||
|
||||
property bool nightModeEnabled: false
|
||||
property bool automationAvailable: false
|
||||
property bool gammaControlAvailable: false
|
||||
|
||||
function updateSingleDevice(device) {
|
||||
const deviceIndex = devices.findIndex(d => d.id === device.id)
|
||||
if (deviceIndex !== -1) {
|
||||
const newDevices = [...devices]
|
||||
const existingDevice = devices[deviceIndex]
|
||||
const cachedMax = deviceMaxCache[device.id]
|
||||
|
||||
let displayMax = cachedMax || (device.class === "ddc" ? device.max : 100)
|
||||
if (displayMax > 0 && !cachedMax) {
|
||||
const newCache = Object.assign({}, deviceMaxCache)
|
||||
newCache[device.id] = displayMax
|
||||
deviceMaxCache = newCache
|
||||
}
|
||||
|
||||
newDevices[deviceIndex] = {
|
||||
"id": device.id,
|
||||
"name": device.id,
|
||||
"class": device.class,
|
||||
"current": device.current,
|
||||
"percentage": device.currentPercent,
|
||||
"max": device.max,
|
||||
"backend": device.backend,
|
||||
"displayMax": displayMax
|
||||
}
|
||||
devices = newDevices
|
||||
}
|
||||
|
||||
const isExponential = SessionData.getBrightnessExponential(device.id)
|
||||
const userSetValue = deviceBrightnessUserSet[device.id]
|
||||
|
||||
let displayValue = device.currentPercent
|
||||
if (isExponential) {
|
||||
if (userSetValue !== undefined) {
|
||||
displayValue = userSetValue
|
||||
} else {
|
||||
displayValue = linearToExponential(device.currentPercent, device.id)
|
||||
}
|
||||
}
|
||||
|
||||
const newBrightness = Object.assign({}, deviceBrightness)
|
||||
newBrightness[device.id] = displayValue
|
||||
deviceBrightness = newBrightness
|
||||
brightnessVersion++
|
||||
}
|
||||
|
||||
function updateFromBrightnessState(state) {
|
||||
if (!state || !state.devices) {
|
||||
return
|
||||
}
|
||||
|
||||
const newMaxCache = Object.assign({}, deviceMaxCache)
|
||||
devices = state.devices.map(d => {
|
||||
const cachedMax = deviceMaxCache[d.id]
|
||||
let displayMax = cachedMax || (d.class === "ddc" ? d.max : 100)
|
||||
if (displayMax > 0 && !cachedMax) {
|
||||
newMaxCache[d.id] = displayMax
|
||||
}
|
||||
return {
|
||||
"id": d.id,
|
||||
"name": d.id,
|
||||
"class": d.class,
|
||||
"current": d.current,
|
||||
"percentage": d.currentPercent,
|
||||
"max": d.max,
|
||||
"backend": d.backend,
|
||||
"displayMax": displayMax
|
||||
}
|
||||
})
|
||||
deviceMaxCache = newMaxCache
|
||||
|
||||
const newBrightness = {}
|
||||
for (const device of state.devices) {
|
||||
const isExponential = SessionData.getBrightnessExponential(device.id)
|
||||
const userSetValue = deviceBrightnessUserSet[device.id]
|
||||
|
||||
if (isExponential) {
|
||||
if (userSetValue !== undefined) {
|
||||
newBrightness[device.id] = userSetValue
|
||||
} else {
|
||||
newBrightness[device.id] = linearToExponential(device.currentPercent, device.id)
|
||||
}
|
||||
} else {
|
||||
newBrightness[device.id] = device.currentPercent
|
||||
}
|
||||
}
|
||||
deviceBrightness = newBrightness
|
||||
brightnessVersion++
|
||||
|
||||
brightnessAvailable = devices.length > 0
|
||||
|
||||
if (devices.length > 0 && !currentDevice) {
|
||||
const lastDevice = SessionData.lastBrightnessDevice || ""
|
||||
const deviceExists = devices.some(d => d.id === lastDevice)
|
||||
if (deviceExists) {
|
||||
setCurrentDevice(lastDevice, false)
|
||||
} else {
|
||||
const backlight = devices.find(d => d.class === "backlight")
|
||||
const nonKbdDevice = devices.find(d => !d.id.includes("kbd"))
|
||||
const defaultDevice = backlight || nonKbdDevice || devices[0]
|
||||
setCurrentDevice(defaultDevice.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!brightnessInitialized) {
|
||||
brightnessInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightness(percentage, device, suppressOsd) {
|
||||
const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice())
|
||||
|
||||
if (!actualDevice) {
|
||||
console.warn("DisplayService: No device selected for brightness change")
|
||||
return
|
||||
}
|
||||
|
||||
const deviceInfo = getCurrentDeviceInfoByName(actualDevice)
|
||||
const isExponential = SessionData.getBrightnessExponential(actualDevice)
|
||||
|
||||
let minValue = 0
|
||||
let maxValue = 100
|
||||
|
||||
if (isExponential) {
|
||||
minValue = 1
|
||||
maxValue = 100
|
||||
} else {
|
||||
minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
|
||||
maxValue = deviceInfo?.displayMax || 100
|
||||
}
|
||||
|
||||
if (maxValue <= 0) {
|
||||
console.warn("DisplayService: Invalid max value for device", actualDevice, "- skipping brightness change")
|
||||
return
|
||||
}
|
||||
|
||||
const clampedValue = Math.max(minValue, Math.min(maxValue, percentage))
|
||||
|
||||
if (!DMSService.isConnected) {
|
||||
console.warn("DisplayService: Not connected to DMS")
|
||||
return
|
||||
}
|
||||
|
||||
const newBrightness = Object.assign({}, deviceBrightness)
|
||||
newBrightness[actualDevice] = clampedValue
|
||||
deviceBrightness = newBrightness
|
||||
brightnessVersion++
|
||||
|
||||
if (isExponential) {
|
||||
const newUserSet = Object.assign({}, deviceBrightnessUserSet)
|
||||
newUserSet[actualDevice] = clampedValue
|
||||
deviceBrightnessUserSet = newUserSet
|
||||
SessionData.setBrightnessUserSetValue(actualDevice, clampedValue)
|
||||
}
|
||||
|
||||
if (!suppressOsd) {
|
||||
brightnessChanged(true)
|
||||
}
|
||||
|
||||
const params = {
|
||||
"device": actualDevice,
|
||||
"percent": clampedValue
|
||||
}
|
||||
if (isExponential) {
|
||||
params.exponential = true
|
||||
params.exponent = SessionData.getBrightnessExponent(actualDevice)
|
||||
}
|
||||
|
||||
DMSService.sendRequest("brightness.setBrightness", params, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set brightness:", response.error)
|
||||
ToastService.showError("Failed to set brightness: " + response.error, "", "", "brightness")
|
||||
} else {
|
||||
ToastService.dismissCategory("brightness")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setCurrentDevice(deviceName, saveToSession = false) {
|
||||
if (currentDevice === deviceName) {
|
||||
return
|
||||
}
|
||||
|
||||
currentDevice = deviceName
|
||||
lastIpcDevice = deviceName
|
||||
|
||||
if (saveToSession) {
|
||||
SessionData.setLastBrightnessDevice(deviceName)
|
||||
}
|
||||
|
||||
deviceSwitched()
|
||||
}
|
||||
|
||||
function getDeviceBrightness(deviceName) {
|
||||
if (!deviceName) {
|
||||
return 50
|
||||
}
|
||||
|
||||
if (deviceName in deviceBrightness) {
|
||||
return deviceBrightness[deviceName]
|
||||
}
|
||||
|
||||
return 50
|
||||
}
|
||||
|
||||
function linearToExponential(linearPercent, deviceName) {
|
||||
const exponent = SessionData.getBrightnessExponent(deviceName)
|
||||
const hardwarePercent = linearPercent / 100.0
|
||||
const normalizedPercent = Math.pow(hardwarePercent, 1.0 / exponent)
|
||||
return Math.round(normalizedPercent * 100.0)
|
||||
}
|
||||
|
||||
function getDefaultDevice() {
|
||||
for (const device of devices) {
|
||||
if (device.class === "backlight") {
|
||||
return device.id
|
||||
}
|
||||
}
|
||||
return devices.length > 0 ? devices[0].id : ""
|
||||
}
|
||||
|
||||
function getCurrentDeviceInfo() {
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
if (!deviceToUse) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.id === deviceToUse) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function isCurrentDeviceReady() {
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
return deviceToUse !== ""
|
||||
}
|
||||
|
||||
function getCurrentDeviceInfoByName(deviceName) {
|
||||
if (!deviceName) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.id === deviceName) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getDeviceMax(deviceName) {
|
||||
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
|
||||
if (!deviceInfo) {
|
||||
return 100
|
||||
}
|
||||
return deviceInfo.displayMax || 100
|
||||
}
|
||||
|
||||
// Night Mode Functions - Simplified
|
||||
function enableNightMode() {
|
||||
if (!gammaControlAvailable) {
|
||||
ToastService.showWarning("Night mode failed: DMS gamma control not available")
|
||||
return
|
||||
}
|
||||
|
||||
nightModeEnabled = true
|
||||
SessionData.setNightModeEnabled(true)
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setEnabled", {
|
||||
"enabled": true
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to enable gamma control:", response.error)
|
||||
ToastService.showError("Failed to enable night mode: " + response.error, "", "", "night-mode")
|
||||
nightModeEnabled = false
|
||||
SessionData.setNightModeEnabled(false)
|
||||
return
|
||||
}
|
||||
ToastService.dismissCategory("night-mode")
|
||||
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
startAutomation()
|
||||
} else {
|
||||
applyNightModeDirectly()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function disableNightMode() {
|
||||
nightModeEnabled = false
|
||||
SessionData.setNightModeEnabled(false)
|
||||
|
||||
if (!gammaControlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setEnabled", {
|
||||
"enabled": false
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to disable gamma control:", response.error)
|
||||
ToastService.showError("Failed to disable night mode: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function toggleNightMode() {
|
||||
if (nightModeEnabled) {
|
||||
disableNightMode()
|
||||
} else {
|
||||
enableNightMode()
|
||||
}
|
||||
}
|
||||
|
||||
function applyNightModeDirectly() {
|
||||
const temperature = SessionData.nightModeTemperature || 4000
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setManualTimes", {
|
||||
"sunrise": null,
|
||||
"sunset": null
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to clear manual times:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to disable IP location:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setTemperature", {
|
||||
"low": temperature,
|
||||
"high": 6500
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set temperature:", response.error)
|
||||
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function startAutomation() {
|
||||
if (!automationAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
const mode = SessionData.nightModeAutoMode || "time"
|
||||
|
||||
switch (mode) {
|
||||
case "time":
|
||||
startTimeBasedMode()
|
||||
break
|
||||
case "location":
|
||||
startLocationBasedMode()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function startTimeBasedMode() {
|
||||
const temperature = SessionData.nightModeTemperature || 4000
|
||||
const highTemp = SessionData.nightModeHighTemperature || 6500
|
||||
const sunriseHour = SessionData.nightModeEndHour
|
||||
const sunriseMinute = SessionData.nightModeEndMinute
|
||||
const sunsetHour = SessionData.nightModeStartHour
|
||||
const sunsetMinute = SessionData.nightModeStartMinute
|
||||
|
||||
const sunrise = `${String(sunriseHour).padStart(2, '0')}:${String(sunriseMinute).padStart(2, '0')}`
|
||||
const sunset = `${String(sunsetHour).padStart(2, '0')}:${String(sunsetMinute).padStart(2, '0')}`
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to disable IP location:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setTemperature", {
|
||||
"low": temperature,
|
||||
"high": highTemp
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set temperature:", response.error)
|
||||
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setManualTimes", {
|
||||
"sunrise": sunrise,
|
||||
"sunset": sunset
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set manual times:", response.error)
|
||||
ToastService.showError("Failed to set night mode schedule: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function startLocationBasedMode() {
|
||||
const temperature = SessionData.nightModeTemperature || 4000
|
||||
const highTemp = SessionData.nightModeHighTemperature || 6500
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setManualTimes", {
|
||||
"sunrise": null,
|
||||
"sunset": null
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to clear manual times:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setTemperature", {
|
||||
"low": temperature,
|
||||
"high": highTemp
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set temperature:", response.error)
|
||||
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
|
||||
return
|
||||
}
|
||||
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": true
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to enable IP location:", response.error)
|
||||
ToastService.showError("Failed to enable IP location: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to disable IP location:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set location:", response.error)
|
||||
ToastService.showError("Failed to set night mode location: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
console.warn("DisplayService: Location mode selected but no coordinates set and IP location disabled")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setNightModeAutomationMode(mode) {
|
||||
SessionData.setNightModeAutoMode(mode)
|
||||
}
|
||||
|
||||
function evaluateNightMode() {
|
||||
if (!nightModeEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
restartTimer.nextAction = "automation"
|
||||
restartTimer.start()
|
||||
} else {
|
||||
restartTimer.nextAction = "direct"
|
||||
restartTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
function checkGammaControlAvailability() {
|
||||
if (!DMSService.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (DMSService.apiVersion < 6) {
|
||||
gammaControlAvailable = false
|
||||
automationAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!DMSService.capabilities.includes("gamma")) {
|
||||
gammaControlAvailable = false
|
||||
automationAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.getState", null, response => {
|
||||
if (response.error) {
|
||||
gammaControlAvailable = false
|
||||
automationAvailable = false
|
||||
console.error("DisplayService: Gamma control not available:", response.error)
|
||||
} else {
|
||||
gammaControlAvailable = true
|
||||
automationAvailable = true
|
||||
|
||||
if (nightModeEnabled) {
|
||||
DMSService.sendRequest("wayland.gamma.setEnabled", {
|
||||
"enabled": true
|
||||
}, enableResponse => {
|
||||
if (enableResponse.error) {
|
||||
console.error("DisplayService: Failed to enable gamma control on startup:", enableResponse.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
startAutomation()
|
||||
} else {
|
||||
applyNightModeDirectly()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: restartTimer
|
||||
property string nextAction: ""
|
||||
interval: 100
|
||||
repeat: false
|
||||
|
||||
onTriggered: {
|
||||
if (nextAction === "automation") {
|
||||
startAutomation()
|
||||
} else if (nextAction === "direct") {
|
||||
applyNightModeDirectly()
|
||||
}
|
||||
nextAction = ""
|
||||
}
|
||||
}
|
||||
|
||||
function rescanDevices() {
|
||||
if (!DMSService.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("brightness.rescan", null, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to rescan brightness devices:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateDeviceBrightnessDisplay(deviceName) {
|
||||
brightnessVersion++
|
||||
brightnessChanged()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
nightModeEnabled = SessionData.nightModeEnabled
|
||||
deviceBrightnessUserSet = Object.assign({}, SessionData.brightnessUserSetValues)
|
||||
if (DMSService.isConnected) {
|
||||
checkGammaControlAvailability()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
|
||||
function onScreensChanged() {
|
||||
rescanDevices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected) {
|
||||
checkGammaControlAvailability()
|
||||
} else {
|
||||
brightnessAvailable = false
|
||||
gammaControlAvailable = false
|
||||
automationAvailable = false
|
||||
}
|
||||
}
|
||||
|
||||
function onCapabilitiesReceived() {
|
||||
checkGammaControlAvailability()
|
||||
}
|
||||
|
||||
function onBrightnessStateUpdate(data) {
|
||||
updateFromBrightnessState(data)
|
||||
}
|
||||
|
||||
function onBrightnessDeviceUpdate(device) {
|
||||
updateSingleDevice(device)
|
||||
}
|
||||
}
|
||||
|
||||
// Session Data Connections
|
||||
Connections {
|
||||
target: SessionData
|
||||
|
||||
function onNightModeEnabledChanged() {
|
||||
nightModeEnabled = SessionData.nightModeEnabled
|
||||
evaluateNightMode()
|
||||
}
|
||||
|
||||
function onNightModeAutoEnabledChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeAutoModeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeStartHourChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeStartMinuteChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeEndHourChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeEndMinuteChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeTemperatureChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeHighTemperatureChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onLatitudeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onLongitudeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeUseIPLocationChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Handler for external control
|
||||
IpcHandler {
|
||||
function set(percentage: string, device: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const value = parseInt(percentage)
|
||||
if (isNaN(value)) {
|
||||
return "Invalid brightness value: " + percentage
|
||||
}
|
||||
|
||||
const targetDevice = device || ""
|
||||
|
||||
if (targetDevice && !root.devices.some(d => d.id === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
const deviceInfo = targetDevice ? root.getCurrentDeviceInfoByName(targetDevice) : null
|
||||
const minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
|
||||
const clampedValue = Math.max(minValue, Math.min(100, value))
|
||||
|
||||
root.lastIpcDevice = targetDevice
|
||||
if (targetDevice && targetDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(targetDevice, false)
|
||||
}
|
||||
root.setBrightness(clampedValue, targetDevice, false)
|
||||
|
||||
if (targetDevice) {
|
||||
return "Brightness set to " + clampedValue + "% on " + targetDevice
|
||||
} else {
|
||||
return "Brightness set to " + clampedValue + "%"
|
||||
}
|
||||
}
|
||||
|
||||
function increment(step: string, device: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const targetDevice = device || ""
|
||||
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
|
||||
|
||||
if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
|
||||
return "Device not found: " + actualDevice
|
||||
}
|
||||
|
||||
const stepValue = parseInt(step || "5")
|
||||
|
||||
root.lastIpcDevice = actualDevice
|
||||
if (actualDevice && actualDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(actualDevice, false)
|
||||
}
|
||||
|
||||
const isExponential = SessionData.getBrightnessExponential(actualDevice)
|
||||
const currentBrightness = root.getDeviceBrightness(actualDevice)
|
||||
const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice)
|
||||
|
||||
let maxValue = 100
|
||||
if (isExponential) {
|
||||
maxValue = 100
|
||||
} else {
|
||||
maxValue = deviceInfo?.displayMax || 100
|
||||
}
|
||||
|
||||
const newBrightness = Math.min(maxValue, currentBrightness + stepValue)
|
||||
|
||||
root.setBrightness(newBrightness, actualDevice, false)
|
||||
|
||||
return "Brightness increased by " + stepValue + "%" + (targetDevice ? " on " + targetDevice : "")
|
||||
}
|
||||
|
||||
function decrement(step: string, device: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const targetDevice = device || ""
|
||||
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
|
||||
|
||||
if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
|
||||
return "Device not found: " + actualDevice
|
||||
}
|
||||
|
||||
const stepValue = parseInt(step || "5")
|
||||
|
||||
root.lastIpcDevice = actualDevice
|
||||
if (actualDevice && actualDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(actualDevice, false)
|
||||
}
|
||||
|
||||
const isExponential = SessionData.getBrightnessExponential(actualDevice)
|
||||
const currentBrightness = root.getDeviceBrightness(actualDevice)
|
||||
const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice)
|
||||
|
||||
let minValue = 0
|
||||
if (isExponential) {
|
||||
minValue = 1
|
||||
} else {
|
||||
minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
|
||||
}
|
||||
|
||||
const newBrightness = Math.max(minValue, currentBrightness - stepValue)
|
||||
|
||||
root.setBrightness(newBrightness, actualDevice, false)
|
||||
|
||||
return "Brightness decreased by " + stepValue + "%" + (targetDevice ? " on " + targetDevice : "")
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%"
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "No brightness devices available"
|
||||
}
|
||||
|
||||
let result = "Available devices:\n"
|
||||
for (const device of root.devices) {
|
||||
const isExp = SessionData.getBrightnessExponential(device.id)
|
||||
result += device.id + " (" + device.class + ")" + (isExp ? " [exponential]" : "") + "\n"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function enableExponential(device: string): string {
|
||||
const targetDevice = device || root.currentDevice
|
||||
if (!targetDevice) {
|
||||
return "No device specified"
|
||||
}
|
||||
|
||||
if (!root.devices.some(d => d.id === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
SessionData.setBrightnessExponential(targetDevice, true)
|
||||
return "Exponential mode enabled for " + targetDevice
|
||||
}
|
||||
|
||||
function disableExponential(device: string): string {
|
||||
const targetDevice = device || root.currentDevice
|
||||
if (!targetDevice) {
|
||||
return "No device specified"
|
||||
}
|
||||
|
||||
if (!root.devices.some(d => d.id === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
SessionData.setBrightnessExponential(targetDevice, false)
|
||||
return "Exponential mode disabled for " + targetDevice
|
||||
}
|
||||
|
||||
function toggleExponential(device: string): string {
|
||||
const targetDevice = device || root.currentDevice
|
||||
if (!targetDevice) {
|
||||
return "No device specified"
|
||||
}
|
||||
|
||||
if (!root.devices.some(d => d.id === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
const currentState = SessionData.getBrightnessExponential(targetDevice)
|
||||
SessionData.setBrightnessExponential(targetDevice, !currentState)
|
||||
return "Exponential mode " + (!currentState ? "enabled" : "disabled") + " for " + targetDevice
|
||||
}
|
||||
|
||||
target: "brightness"
|
||||
}
|
||||
|
||||
// IPC Handler for night mode control
|
||||
IpcHandler {
|
||||
function toggle(): string {
|
||||
root.toggleNightMode()
|
||||
return root.nightModeEnabled ? "Night mode enabled" : "Night mode disabled"
|
||||
}
|
||||
|
||||
function enable(): string {
|
||||
root.enableNightMode()
|
||||
return "Night mode enabled"
|
||||
}
|
||||
|
||||
function disable(): string {
|
||||
root.disableNightMode()
|
||||
return "Night mode disabled"
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
return root.nightModeEnabled ? "Night mode is enabled" : "Night mode is disabled"
|
||||
}
|
||||
|
||||
function temperature(value: string): string {
|
||||
if (!value) {
|
||||
return "Current temperature: " + SessionData.nightModeTemperature + "K"
|
||||
}
|
||||
|
||||
const temp = parseInt(value)
|
||||
if (isNaN(temp)) {
|
||||
return "Invalid temperature. Use a value between 2500 and 6000 (in steps of 500)"
|
||||
}
|
||||
|
||||
// Validate temperature is in valid range and steps
|
||||
if (temp < 2500 || temp > 6000) {
|
||||
return "Temperature must be between 2500K and 6000K"
|
||||
}
|
||||
|
||||
// Round to nearest 500
|
||||
const rounded = Math.round(temp / 500) * 500
|
||||
|
||||
SessionData.setNightModeTemperature(rounded)
|
||||
|
||||
// Restart night mode with new temperature if active
|
||||
if (root.nightModeEnabled) {
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
root.startAutomation()
|
||||
} else {
|
||||
root.applyNightModeDirectly()
|
||||
}
|
||||
}
|
||||
|
||||
if (rounded !== temp) {
|
||||
return "Night mode temperature set to " + rounded + "K (rounded from " + temp + "K)"
|
||||
} else {
|
||||
return "Night mode temperature set to " + rounded + "K"
|
||||
}
|
||||
}
|
||||
|
||||
target: "night"
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool dwlAvailable: false
|
||||
property var outputs: ({})
|
||||
property var tagCount: 9
|
||||
property var layouts: []
|
||||
property string activeOutput: ""
|
||||
property var outputScales: ({})
|
||||
|
||||
signal stateChanged()
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
function onCapabilitiesReceived() {
|
||||
checkCapabilities()
|
||||
}
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected) {
|
||||
checkCapabilities()
|
||||
} else {
|
||||
dwlAvailable = false
|
||||
}
|
||||
}
|
||||
function onDwlStateUpdate(data) {
|
||||
if (dwlAvailable) {
|
||||
handleStateUpdate(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (DMSService.dmsAvailable) {
|
||||
checkCapabilities()
|
||||
}
|
||||
if (dwlAvailable) {
|
||||
refreshOutputScales()
|
||||
}
|
||||
}
|
||||
|
||||
function checkCapabilities() {
|
||||
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
|
||||
dwlAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
const hasDwl = DMSService.capabilities.includes("dwl")
|
||||
if (hasDwl && !dwlAvailable) {
|
||||
dwlAvailable = true
|
||||
console.info("DwlService: DWL capability detected")
|
||||
requestState()
|
||||
refreshOutputScales()
|
||||
} else if (!hasDwl) {
|
||||
dwlAvailable = false
|
||||
}
|
||||
}
|
||||
|
||||
function requestState() {
|
||||
if (!DMSService.isConnected || !dwlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("dwl.getState", null, response => {
|
||||
if (response.result) {
|
||||
handleStateUpdate(response.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleStateUpdate(state) {
|
||||
outputs = state.outputs || {}
|
||||
tagCount = state.tagCount || 9
|
||||
layouts = state.layouts || []
|
||||
activeOutput = state.activeOutput || ""
|
||||
stateChanged()
|
||||
}
|
||||
|
||||
function setTags(outputName, tagmask, toggleTagset) {
|
||||
if (!DMSService.isConnected || !dwlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("dwl.setTags", {
|
||||
"output": outputName,
|
||||
"tagmask": tagmask,
|
||||
"toggleTagset": toggleTagset
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("DwlService: setTags error:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setClientTags(outputName, andTags, xorTags) {
|
||||
if (!DMSService.isConnected || !dwlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("dwl.setClientTags", {
|
||||
"output": outputName,
|
||||
"andTags": andTags,
|
||||
"xorTags": xorTags
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("DwlService: setClientTags error:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setLayout(outputName, index) {
|
||||
if (!DMSService.isConnected || !dwlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("dwl.setLayout", {
|
||||
"output": outputName,
|
||||
"index": index
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("DwlService: setLayout error:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getOutputState(outputName) {
|
||||
if (!outputs || !outputs[outputName]) {
|
||||
return null
|
||||
}
|
||||
return outputs[outputName]
|
||||
}
|
||||
|
||||
function getActiveTags(outputName) {
|
||||
const output = getOutputState(outputName)
|
||||
if (!output || !output.tags) {
|
||||
return []
|
||||
}
|
||||
return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag)
|
||||
}
|
||||
|
||||
function getTagsWithClients(outputName) {
|
||||
const output = getOutputState(outputName)
|
||||
if (!output || !output.tags) {
|
||||
return []
|
||||
}
|
||||
return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag)
|
||||
}
|
||||
|
||||
function getUrgentTags(outputName) {
|
||||
const output = getOutputState(outputName)
|
||||
if (!output || !output.tags) {
|
||||
return []
|
||||
}
|
||||
return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag)
|
||||
}
|
||||
|
||||
function switchToTag(outputName, tagIndex) {
|
||||
const tagmask = 1 << tagIndex
|
||||
setTags(outputName, tagmask, 0)
|
||||
}
|
||||
|
||||
function toggleTag(outputName, tagIndex) {
|
||||
const output = getOutputState(outputName)
|
||||
if (!output || !output.tags) {
|
||||
console.log("toggleTag: no output or tags for", outputName)
|
||||
return
|
||||
}
|
||||
|
||||
let currentMask = 0
|
||||
output.tags.forEach(tag => {
|
||||
if (tag.state === 1) {
|
||||
currentMask |= (1 << tag.tag)
|
||||
}
|
||||
})
|
||||
|
||||
const clickedMask = 1 << tagIndex
|
||||
const newMask = currentMask ^ clickedMask
|
||||
|
||||
console.log("toggleTag:", outputName, "tag:", tagIndex, "currentMask:", currentMask.toString(2), "clickedMask:", clickedMask.toString(2), "newMask:", newMask.toString(2))
|
||||
|
||||
if (newMask === 0) {
|
||||
console.log("toggleTag: newMask is 0, switching to tag", tagIndex)
|
||||
setTags(outputName, 1 << tagIndex, 0)
|
||||
} else {
|
||||
console.log("toggleTag: setting combined mask", newMask)
|
||||
setTags(outputName, newMask, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function quit() {
|
||||
Quickshell.execDetached(["mmsg", "-d", "quit"])
|
||||
}
|
||||
|
||||
Process {
|
||||
id: scaleQueryProcess
|
||||
command: ["mmsg", "-A"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const newScales = {}
|
||||
const lines = text.trim().split('\n')
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
if (parts.length >= 3 && parts[1] === "scale_factor") {
|
||||
const outputName = parts[0]
|
||||
const scale = parseFloat(parts[2])
|
||||
if (!isNaN(scale)) {
|
||||
newScales[outputName] = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
outputScales = newScales
|
||||
} catch (e) {
|
||||
console.warn("DwlService: Failed to parse mmsg output:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DwlService: mmsg failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshOutputScales() {
|
||||
if (!dwlAvailable) return
|
||||
scaleQueryProcess.running = true
|
||||
}
|
||||
|
||||
function getOutputScale(outputName) {
|
||||
return outputScales[outputName]
|
||||
}
|
||||
|
||||
function getVisibleTags(outputName) {
|
||||
const output = getOutputState(outputName)
|
||||
if (!output || !output.tags) {
|
||||
return []
|
||||
}
|
||||
|
||||
const visibleTags = new Set()
|
||||
|
||||
output.tags.forEach(tag => {
|
||||
if (tag.state === 1 || tag.clients > 0) {
|
||||
visibleTags.add(tag.tag)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(visibleTags).sort((a, b) => a - b)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string currentProvider: "hyprland"
|
||||
property var keybinds: ({"title": "", "provider": "", "binds": []})
|
||||
|
||||
Process {
|
||||
id: getKeybinds
|
||||
running: false
|
||||
command: ["dms", "keybinds", "show", root.currentProvider]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
root.keybinds = JSON.parse(text)
|
||||
} catch (e) {
|
||||
console.error("[KeybindsService] Error parsing keybinds:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (code) => {
|
||||
if (code !== 0 && code !== 15) {
|
||||
console.warn("[KeybindsService] Process exited with code:", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 500
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
getKeybinds.running = true
|
||||
}
|
||||
}
|
||||
|
||||
function loadProvider(provider) {
|
||||
root.currentProvider = provider
|
||||
reload()
|
||||
}
|
||||
|
||||
function reload() {
|
||||
if (getKeybinds.running) {
|
||||
getKeybinds.running = false
|
||||
}
|
||||
Qt.callLater(function() {
|
||||
getKeybinds.running = true
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user