mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
948 Commits
more-loadi
...
monorepo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50cdd68b7b | ||
|
|
e8510b925e | ||
|
|
24e800501a | ||
|
|
6013c994a6 | ||
|
|
46c90628b9 | ||
|
|
d2d2dac5d1 | ||
|
|
fd3e7470f4 | ||
|
|
b79e9f72ce | ||
|
|
77eb5dd3bf | ||
|
|
b17c14a07b | ||
|
|
494d90be22 | ||
|
|
da7e599e65 | ||
|
|
e3b7360f39 | ||
|
|
367130882d | ||
|
|
d8563ba79d | ||
|
|
e527453964 | ||
|
|
88fe3c5fbd | ||
|
|
748faf92c1 | ||
|
|
0126aded78 | ||
|
|
695a75ea09 | ||
|
|
80e690f9fc | ||
|
|
e8770b90ef | ||
|
|
eec9da42bf | ||
|
|
1c8f0d6292 | ||
|
|
b753c8840b | ||
|
|
95589982a5 | ||
|
|
37a10bd453 | ||
|
|
7abc76e92c | ||
|
|
7aa4467bda | ||
|
|
471938adb6 | ||
|
|
201a7e3b34 | ||
|
|
11ec3723c3 | ||
|
|
75eb736856 | ||
|
|
8fea126c20 | ||
|
|
cc02d09c4d | ||
|
|
af95631a1d | ||
|
|
7b3d2ab85a | ||
|
|
c52df96af9 | ||
|
|
dee5fa60af | ||
|
|
5e99fdd9c9 | ||
|
|
eb01fe757b | ||
|
|
c52483da2c | ||
|
|
2714c0f4ad | ||
|
|
bba21408ea | ||
|
|
47c5320d67 | ||
|
|
b5c49573e5 | ||
|
|
0197961175 | ||
|
|
f08b98dcba | ||
|
|
3878998080 | ||
|
|
5fa117db4c | ||
|
|
caa085a646 | ||
|
|
392a1c03c5 | ||
|
|
1524d27f4c | ||
|
|
d309957927 | ||
|
|
e0f2c03b91 | ||
|
|
1e5848e0d5 | ||
|
|
18bb7dc47b | ||
|
|
0ea7de12a5 | ||
|
|
c8e382e2dd | ||
|
|
84e19f8565 | ||
|
|
f597ea9948 | ||
|
|
d43e1a7cbe | ||
|
|
8131e713cf | ||
|
|
fefa2bd839 | ||
|
|
cc0984db14 | ||
|
|
f87609417b | ||
|
|
8d57b55f94 | ||
|
|
55776fd7cb | ||
|
|
3963c98689 | ||
|
|
02c59636fc | ||
|
|
989f196894 | ||
|
|
9314de4772 | ||
|
|
44a6cd88cd | ||
|
|
d8774c4787 | ||
|
|
a56066bac1 | ||
|
|
8a96f71d10 | ||
|
|
20a684e8f5 | ||
|
|
c8fcf50095 | ||
|
|
58b637bcca | ||
|
|
86caf92c90 | ||
|
|
35ead280d5 | ||
|
|
5d6c3e364d | ||
|
|
f006175829 | ||
|
|
3e0f325734 | ||
|
|
f8d383cff0 | ||
|
|
f2ec3ae755 | ||
|
|
f95e4e016b | ||
|
|
898e9e67d0 | ||
|
|
15983921b0 | ||
|
|
65c2077e30 | ||
|
|
946a28d3be | ||
|
|
69accb5319 | ||
|
|
4ddcf4391a | ||
|
|
a0d886009a | ||
|
|
91c37aaa96 | ||
|
|
7602247558 | ||
|
|
d5a4035bef | ||
|
|
d9652c7334 | ||
|
|
9b4fd7449b | ||
|
|
bc6b568f7e | ||
|
|
c9ee856f91 | ||
|
|
2355c898f8 | ||
|
|
87d0aed4ad | ||
|
|
3cbc547e0f | ||
|
|
e883ebe307 | ||
|
|
20d797320a | ||
|
|
bc3e043192 | ||
|
|
c0555aa608 | ||
|
|
e75b47c21a | ||
|
|
3702f493f6 | ||
|
|
80d88d4d8f | ||
|
|
95c711ce7e | ||
|
|
0ee89920fd | ||
|
|
fd49a171c0 | ||
|
|
16ad5221eb | ||
|
|
8167c432b8 | ||
|
|
ae5d6c1ba4 | ||
|
|
1e6c80bd03 | ||
|
|
d48cd1acdd | ||
|
|
a5f92165fb | ||
|
|
d834124a71 | ||
|
|
ce6f3afb39 | ||
|
|
f5462fa1bf | ||
|
|
f75e23158a | ||
|
|
253ff71a0a | ||
|
|
8d7db49cb0 | ||
|
|
315509f7a4 | ||
|
|
a7bd8b810b | ||
|
|
a7c8ba332b | ||
|
|
40cadb6a00 | ||
|
|
cbf409dffc | ||
|
|
60a791442e | ||
|
|
528e8bf92e | ||
|
|
8a4243e7f8 | ||
|
|
1ac95f0d14 | ||
|
|
cd51eb25ce | ||
|
|
4e64a2b2b2 | ||
|
|
672c660c41 | ||
|
|
2a56e57490 | ||
|
|
f6efd2363a | ||
|
|
fa08b39bb0 | ||
|
|
81c3110d0d | ||
|
|
c01e636421 | ||
|
|
fd8d2961bf | ||
|
|
9e4b53e20b | ||
|
|
20116b3933 | ||
|
|
bca5ee0c0d | ||
|
|
331bd69021 | ||
|
|
57b11b7699 | ||
|
|
3e9b11c281 | ||
|
|
bbfd618626 | ||
|
|
00abb839f9 | ||
|
|
1d639d5f5a | ||
|
|
c565fc08c3 | ||
|
|
026c71f9fc | ||
|
|
1eed499151 | ||
|
|
21f2aabd58 | ||
|
|
e1f06b7139 | ||
|
|
c4be74bce5 | ||
|
|
7c9e9e1cd9 | ||
|
|
797aabc637 | ||
|
|
630a3d4845 | ||
|
|
3b0bb4ea74 | ||
|
|
e36347a4c3 | ||
|
|
4f59dfc49c | ||
|
|
fc0082a470 | ||
|
|
712449674f | ||
|
|
a64b4527f2 | ||
|
|
71a7ebbfe2 | ||
|
|
d6d701c722 | ||
|
|
43fbbc07f5 | ||
|
|
8504144c32 | ||
|
|
1d3e59b5dd | ||
|
|
3f70ca3506 | ||
|
|
3640d8bd24 | ||
|
|
706a99817f | ||
|
|
4645b2dcab | ||
|
|
13f1673371 | ||
|
|
7d374c4c2a | ||
|
|
5ed449773c | ||
|
|
aef9c2269a | ||
|
|
daa5a3e821 | ||
|
|
5cd1167b28 | ||
|
|
21e7ae3dfd | ||
|
|
5d40138585 | ||
|
|
893fd820a3 | ||
|
|
2d536d99e5 | ||
|
|
a0ee4792b9 | ||
|
|
a8f6880840 | ||
|
|
51296d1d44 | ||
|
|
0f29149014 | ||
|
|
b9f0c277ec | ||
|
|
69964c9704 | ||
|
|
ff1d38e34f | ||
|
|
3abee7f2f5 | ||
|
|
ed0b80008f | ||
|
|
976ff108b3 | ||
|
|
66e3cc77c5 | ||
|
|
229abba1e4 | ||
|
|
8dacaf84cc | ||
|
|
102b185572 | ||
|
|
52ac474f7d | ||
|
|
c0064cfcfa | ||
|
|
414ce5610d | ||
|
|
113ac42814 | ||
|
|
a2f2eef326 | ||
|
|
54a69a6101 | ||
|
|
5e2d3c8d7d | ||
|
|
5a9950a7c3 | ||
|
|
2aadbc1a61 | ||
|
|
749414ab65 | ||
|
|
baaebcd413 | ||
|
|
5a8a60b15d | ||
|
|
f0ddb8db49 | ||
|
|
baa12c0161 | ||
|
|
ca226e98c2 | ||
|
|
453079ef1f | ||
|
|
074aea2c35 | ||
|
|
9cf5f0b9b3 | ||
|
|
89e12eea29 | ||
|
|
03d4caff8f | ||
|
|
89d54dedb7 | ||
|
|
9a9e62ccd3 | ||
|
|
eca38ae920 | ||
|
|
84e89599bf | ||
|
|
e1cdf4ed50 | ||
|
|
e4371ea4fc | ||
|
|
9c45d13cbf | ||
|
|
5f22347d7a | ||
|
|
ca786a3567 | ||
|
|
60ce662d49 | ||
|
|
4f9b0d8925 | ||
|
|
9c2fc570e6 | ||
|
|
0ba982b271 | ||
|
|
ff3123e387 | ||
|
|
1548286083 | ||
|
|
c018d953b8 | ||
|
|
cf66d28774 | ||
|
|
9cec6fd212 | ||
|
|
92926331b5 | ||
|
|
f9932ea222 | ||
|
|
a65d6b7630 | ||
|
|
7252d1e4d7 | ||
|
|
3b5a951431 | ||
|
|
0b1c331705 | ||
|
|
3c354b71f5 | ||
|
|
1eb5f381ae | ||
|
|
c5efd28781 | ||
|
|
53ae8ac917 | ||
|
|
505b6368e6 | ||
|
|
3c20e9e203 | ||
|
|
43427461f5 | ||
|
|
d7740ff6d2 | ||
|
|
1fb4eb33aa | ||
|
|
b27f362b44 | ||
|
|
325e3bc19b | ||
|
|
9215985335 | ||
|
|
293179daa6 | ||
|
|
4fe79dbe85 | ||
|
|
55d738e917 | ||
|
|
986b07f4a9 | ||
|
|
450c2e91ed | ||
|
|
4d06333624 | ||
|
|
fbe4122404 | ||
|
|
baf9b5e6f3 | ||
|
|
c88fc20701 | ||
|
|
b1078d6c73 | ||
|
|
5033d10246 | ||
|
|
986993a890 | ||
|
|
19b13a1e81 | ||
|
|
76637fab33 | ||
|
|
0a79d9a187 | ||
|
|
36b3b3c7ae | ||
|
|
8caeca0c08 | ||
|
|
1c323f54ee | ||
|
|
7ed0b752a8 | ||
|
|
0569906f7c | ||
|
|
2a7cf187ad | ||
|
|
cc5b98a5d2 | ||
|
|
1478c92f49 | ||
|
|
e1785a1738 | ||
|
|
44ebd2918c | ||
|
|
c87fa0de5e | ||
|
|
7b26692c8e | ||
|
|
b294e391e7 | ||
|
|
85f8e362e6 | ||
|
|
d68a6a1056 | ||
|
|
3dae9c0639 | ||
|
|
aede6b064a | ||
|
|
76b168020c | ||
|
|
5e36b1454a | ||
|
|
bd35fbac4d | ||
|
|
e081ec19cc | ||
|
|
d870d8bad6 | ||
|
|
20fd13c836 | ||
|
|
59f98b151d | ||
|
|
4ac1990c12 | ||
|
|
0a5105cc62 | ||
|
|
a9f8b835ee | ||
|
|
0109bd5bda | ||
|
|
01dad64c6d | ||
|
|
ee38f57f6d | ||
|
|
6b163dcb5f | ||
|
|
baadbbc65a | ||
|
|
13a2813db9 | ||
|
|
cfa7d12dd3 | ||
|
|
8bf23d820f | ||
|
|
3c7e903ace | ||
|
|
ee0e3aece9 | ||
|
|
d7efd1b285 | ||
|
|
34f7a7ab18 | ||
|
|
695eb0a401 | ||
|
|
0d44b95a40 | ||
|
|
116c421492 | ||
|
|
53507ef56b | ||
|
|
3c049e031f | ||
|
|
b6688adb35 | ||
|
|
b46fe28c05 | ||
|
|
e7debdcf46 | ||
|
|
2c2930e876 | ||
|
|
ca294fc049 | ||
|
|
86d1a40299 | ||
|
|
7a3884a633 | ||
|
|
7e5c6581c9 | ||
|
|
f17bbbd689 | ||
|
|
24b046e9d7 | ||
|
|
48a7d24c11 | ||
|
|
033f96a4b0 | ||
|
|
f0a1cb6525 | ||
|
|
db5782783b | ||
|
|
29022e260d | ||
|
|
1e1f58d3ed | ||
|
|
12389e2856 | ||
|
|
cde7427449 | ||
|
|
42e7cb7b5f | ||
|
|
d7992bc1f7 | ||
|
|
61c8549401 | ||
|
|
a284dcf61d | ||
|
|
2e462b0899 | ||
|
|
b79c66d59a | ||
|
|
2f2020e7e2 | ||
|
|
b7e99c0d2b | ||
|
|
2648848898 | ||
|
|
79b23ca829 | ||
|
|
0ac5b7bc87 | ||
|
|
1d211e8474 | ||
|
|
1981a83e82 | ||
|
|
cac071e7af | ||
|
|
c6efccd61c | ||
|
|
a90b00e5fe | ||
|
|
7863d03282 | ||
|
|
968606d781 | ||
|
|
f7e8de2556 | ||
|
|
17a8edc1ae | ||
|
|
30dc63c801 | ||
|
|
8db7b8419a | ||
|
|
8c626b20e1 | ||
|
|
a8929c8046 | ||
|
|
f8e4b5e958 | ||
|
|
58cae24157 | ||
|
|
bb4f5f37cc | ||
|
|
237333941a | ||
|
|
e1406875aa | ||
|
|
6ab96898a3 | ||
|
|
7973b2f3da | ||
|
|
90284625af | ||
|
|
6b3442512a | ||
|
|
f9208136af | ||
|
|
9895fbde0d | ||
|
|
30370e4985 | ||
|
|
7a45f370b5 | ||
|
|
38068eeaac | ||
|
|
62df30ed6c | ||
|
|
7e75c9e510 | ||
|
|
42f9edf566 | ||
|
|
c72b6144a5 | ||
|
|
032777e32e | ||
|
|
607b5320fd | ||
|
|
959766b265 | ||
|
|
9774991b56 | ||
|
|
e1587995d0 | ||
|
|
08e6e22046 | ||
|
|
454d8bdc88 | ||
|
|
d0ae7431eb | ||
|
|
d88dc17b21 | ||
|
|
705f569571 | ||
|
|
abc7badfa9 | ||
|
|
e8c2469227 | ||
|
|
1e5e8cd246 | ||
|
|
adc81cfb95 | ||
|
|
e175fa64cb | ||
|
|
f9994d0e42 | ||
|
|
3e5d1c514a | ||
|
|
6310394034 | ||
|
|
f32596053b | ||
|
|
cf4a6969d3 | ||
|
|
0918412916 | ||
|
|
41b1718587 | ||
|
|
ca2acbc704 | ||
|
|
1abd3ef8b1 | ||
|
|
cedba3770c | ||
|
|
f733be1fd1 | ||
|
|
01e02232d7 | ||
|
|
771920b38b | ||
|
|
0f55bbc148 | ||
|
|
ab4e9646ad | ||
|
|
884b73599a | ||
|
|
492c0e7ef7 | ||
|
|
0865ae000b | ||
|
|
049c9b44e4 | ||
|
|
199edd3771 | ||
|
|
8806217d25 | ||
|
|
cc1588debd | ||
|
|
d2ba4b32fe | ||
|
|
b3d5054966 | ||
|
|
57a921425c | ||
|
|
061aaeb933 | ||
|
|
0c7af9c740 | ||
|
|
d5c4b990dc | ||
|
|
a650a79dfc | ||
|
|
7ac6e94348 | ||
|
|
b4abdf3d51 | ||
|
|
b59b87d84e | ||
|
|
799ae1a20e | ||
|
|
1e58e69c59 | ||
|
|
c667bab5ca | ||
|
|
2a744fb174 | ||
|
|
a9744a0cad | ||
|
|
0aab22f242 | ||
|
|
b0f65225a9 | ||
|
|
1311da7258 | ||
|
|
61d68b1f76 | ||
|
|
5dd1769536 | ||
|
|
a45a9bda9e | ||
|
|
4e43c797e2 | ||
|
|
beab1a7b01 | ||
|
|
85c00a9c4e | ||
|
|
b2e5565110 | ||
|
|
95785afec9 | ||
|
|
d9d16eccfe | ||
|
|
26900c9b62 | ||
|
|
8113ddc809 | ||
|
|
3cd6a1a558 | ||
|
|
9128141be0 | ||
|
|
0b0af20a84 | ||
|
|
225144cb46 | ||
|
|
bbe802037e | ||
|
|
1db4e92779 | ||
|
|
072883dcd4 | ||
|
|
a25e929200 | ||
|
|
6c4d27be8a | ||
|
|
8825382502 | ||
|
|
9ce3c5bd73 | ||
|
|
771346c8fa | ||
|
|
a56b2d6a9f | ||
|
|
342f980bad | ||
|
|
dbc1bdeb3b | ||
|
|
1cb10e879e | ||
|
|
d90dd5288b | ||
|
|
e55a517dae | ||
|
|
378def1fa3 | ||
|
|
ea7676c98e | ||
|
|
b9b15568b4 | ||
|
|
701d8cbd8a | ||
|
|
bd525763de | ||
|
|
479868718e | ||
|
|
951136bc4c | ||
|
|
8ab25ef8e4 | ||
|
|
a11cd9b0df | ||
|
|
1e72733e81 | ||
|
|
967b7d05de | ||
|
|
90bc890190 | ||
|
|
647c358b72 | ||
|
|
2a89885437 | ||
|
|
47cc43185d | ||
|
|
aa6e09ed3e | ||
|
|
c7bc3d6f3b | ||
|
|
0b68bf7c07 | ||
|
|
3274ef5e3e | ||
|
|
f93a00c8d4 | ||
|
|
92bcb83b16 | ||
|
|
4e0c813db7 | ||
|
|
d4509c80b7 | ||
|
|
c9313df3a4 | ||
|
|
9b6fb29d46 | ||
|
|
50ce5cf257 | ||
|
|
b19e5b3b40 | ||
|
|
c07ba3f737 | ||
|
|
eff5f60264 | ||
|
|
355b2e16b4 | ||
|
|
c389101a10 | ||
|
|
4aa0b3d0fc | ||
|
|
322f1415f6 | ||
|
|
0d57691e38 | ||
|
|
507b516f89 | ||
|
|
7bf73ab14d | ||
|
|
9a305355c2 | ||
|
|
6ac382a25f | ||
|
|
e02b255442 | ||
|
|
d978740d66 | ||
|
|
62b7492e9f | ||
|
|
c2f42f3f69 | ||
|
|
2c4a40e778 | ||
|
|
635fcad416 | ||
|
|
2c92f830d1 | ||
|
|
4924f3e55a | ||
|
|
53306165e1 | ||
|
|
1ebcdaaf62 | ||
|
|
078ef203b6 | ||
|
|
59669d8b7f | ||
|
|
d38b98459a | ||
|
|
f54e53b8a0 | ||
|
|
851d47213c | ||
|
|
ad778b5d81 | ||
|
|
0cb081a6d0 | ||
|
|
daf3525e80 | ||
|
|
b35198c710 | ||
|
|
1feb77aadb | ||
|
|
d6b690ae2f | ||
|
|
b1ae246c86 | ||
|
|
4ceb5f13e5 | ||
|
|
64960e4dcd | ||
|
|
e1c180a13f | ||
|
|
86a0fd409a | ||
|
|
5a32398446 | ||
|
|
bcb22ec265 | ||
|
|
8719dcf98f | ||
|
|
9b4b2f75c1 | ||
|
|
8acde3a347 | ||
|
|
7c1e247ef8 | ||
|
|
f4cd27d316 | ||
|
|
205c43181b | ||
|
|
05a72abf41 | ||
|
|
14262ba510 | ||
|
|
d847b1e09c | ||
|
|
0086e42a86 | ||
|
|
7474d5a7bf | ||
|
|
5696a36115 | ||
|
|
3cdc1a9c81 | ||
|
|
b095fb9005 | ||
|
|
ce6c16214c | ||
|
|
b6f7f2734e | ||
|
|
4db55e4d77 | ||
|
|
b21f6e80b3 | ||
|
|
a804fb849e | ||
|
|
4ca91cd9f7 | ||
|
|
16e1b587b4 | ||
|
|
5e2756d200 | ||
|
|
ce9ab22ae1 | ||
|
|
72ad35e1f9 | ||
|
|
c0d110cde0 | ||
|
|
b9d5deb2ae | ||
|
|
d4b13ef46b | ||
|
|
748d9e342e | ||
|
|
f49312fc0e | ||
|
|
e0d8bbb243 | ||
|
|
153f2a49f8 | ||
|
|
8b272dc2fd | ||
|
|
87a919bbde | ||
|
|
d3017e98c5 | ||
|
|
5758d7274e | ||
|
|
0e215d69cb | ||
|
|
cbaaa32ce8 | ||
|
|
5c81646397 | ||
|
|
30ca1fb14f | ||
|
|
9fab49984a | ||
|
|
696fa6e4f8 | ||
|
|
921393e84e | ||
|
|
13e894e910 | ||
|
|
7c7e8aaef3 | ||
|
|
7ea3bd9df9 | ||
|
|
7bf7d0afae | ||
|
|
0d329baaca | ||
|
|
941a87b59c | ||
|
|
a9e8ac46d8 | ||
|
|
0d3a294118 | ||
|
|
7a27537632 | ||
|
|
287e778ddb | ||
|
|
ce44edb419 | ||
|
|
9dcd8af7a3 | ||
|
|
76dfcd0ccb | ||
|
|
13a188635d | ||
|
|
cd18fd5aed | ||
|
|
b277bd8014 | ||
|
|
daa0d368ab | ||
|
|
2cc7777e16 | ||
|
|
d276e31f7b | ||
|
|
7f35ba7e21 | ||
|
|
edd54dda84 | ||
|
|
a50a97314d | ||
|
|
4bc05e7083 | ||
|
|
09a45b49a6 | ||
|
|
1c0b71436e | ||
|
|
24f5e9a7e6 | ||
|
|
59d123a4a1 | ||
|
|
ed2afa03f9 | ||
|
|
3c531dc2ec | ||
|
|
83564bd03f | ||
|
|
7146d0d92d | ||
|
|
e842d6761a | ||
|
|
17b405e9dc | ||
|
|
f281513a41 | ||
|
|
63b876479f | ||
|
|
38b833c886 | ||
|
|
d75ea18e9f | ||
|
|
f311b20ef7 | ||
|
|
78f7237422 | ||
|
|
726af3393b | ||
|
|
c772331554 | ||
|
|
80d257b94f | ||
|
|
e2db034959 | ||
|
|
c4e88e5c05 | ||
|
|
e47e7667c6 | ||
|
|
8bb2a64663 | ||
|
|
e056e08fc1 | ||
|
|
342cd55bc0 | ||
|
|
23ef19e683 | ||
|
|
437fd29e96 | ||
|
|
aa7a07fd99 | ||
|
|
5217006dec | ||
|
|
ab4f6baae6 | ||
|
|
1976ea4d49 | ||
|
|
697fc4d2b7 | ||
|
|
38c1f7bbcb | ||
|
|
8cbfaab807 | ||
|
|
f4a4151632 | ||
|
|
5f810fe741 | ||
|
|
adaa0caab8 | ||
|
|
54ef14e765 | ||
|
|
d1383b5d1b | ||
|
|
caa703af99 | ||
|
|
90aab9f4db | ||
|
|
3439030145 | ||
|
|
058c7408d1 | ||
|
|
a4f7fd58f6 | ||
|
|
6f3024c90d | ||
|
|
5f95fa5e79 | ||
|
|
f9cb0506e9 | ||
|
|
2429401d0e | ||
|
|
9ff0d7405f | ||
|
|
5bb5cd296d | ||
|
|
273662e03e | ||
|
|
9c1a89d786 | ||
|
|
524d7ee5c0 | ||
|
|
51d2bc9aae | ||
|
|
0c8a7ff332 | ||
|
|
309b8d9efe | ||
|
|
c2f32b7bdc | ||
|
|
563bc7b359 | ||
|
|
94ca5a5bef | ||
|
|
4464589c0f | ||
|
|
748d4fe2ac | ||
|
|
50b28dc8ca | ||
|
|
118980a9fb | ||
|
|
8aff381676 | ||
|
|
6d0fba1905 | ||
|
|
7e885d3cee | ||
|
|
811daf74ff | ||
|
|
ee755b8bd6 | ||
|
|
1019eb925a | ||
|
|
061bb50b88 | ||
|
|
eb7e665c86 | ||
|
|
80301d1aab | ||
|
|
ea56fb5840 | ||
|
|
692b45c4f0 | ||
|
|
8d53a8826e | ||
|
|
64ea115303 | ||
|
|
b5e29cf50c | ||
|
|
381df1e949 | ||
|
|
13a81eda6f | ||
|
|
a48c39642a | ||
|
|
3be3e622bc | ||
|
|
07fe2ca407 | ||
|
|
9b96dae744 | ||
|
|
0e3d3d1a40 | ||
|
|
cb3274fb0c | ||
|
|
a3ada5b2bb | ||
|
|
3e167a2c52 | ||
|
|
38b3ad2b31 | ||
|
|
56e5cd13b7 | ||
|
|
d63c0fc6f0 | ||
|
|
6814b140fc | ||
|
|
5f7e478118 | ||
|
|
7317024da5 | ||
|
|
9b9fbabc3f | ||
|
|
3c5a23799f | ||
|
|
3bfdc6163c | ||
|
|
cf2f74a38d | ||
|
|
2a7f52c67e | ||
|
|
50fde1e308 | ||
|
|
46fd0ae413 | ||
|
|
65e32dc429 | ||
|
|
413675dfc1 | ||
|
|
a17343f40e | ||
|
|
5d023804c1 | ||
|
|
fa07a846b9 | ||
|
|
5df46b605e | ||
|
|
77cf371a21 | ||
|
|
89802dd040 | ||
|
|
59b95e9dd6 | ||
|
|
b836db5252 | ||
|
|
4dc4b15925 | ||
|
|
5c3062e699 | ||
|
|
3a7777c643 | ||
|
|
71543c35d6 | ||
|
|
f4cf66dc01 | ||
|
|
7870dff0fd | ||
|
|
9fc9c1ed19 | ||
|
|
4d0151350f | ||
|
|
dff10c8d13 | ||
|
|
362bcb9294 | ||
|
|
351b4f8a94 | ||
|
|
90955eb0a1 | ||
|
|
62669747ad | ||
|
|
466d00c666 | ||
|
|
63845ff875 | ||
|
|
d013748a51 | ||
|
|
474af3bc07 | ||
|
|
8e09e155fa | ||
|
|
7f9f4f96b9 | ||
|
|
cd488a8623 | ||
|
|
080b7a28b1 | ||
|
|
6949ed0ebd | ||
|
|
8465fa45bb | ||
|
|
40835ffc89 | ||
|
|
01a42ff330 | ||
|
|
ba49654a64 | ||
|
|
bc6577fe18 | ||
|
|
4ca3f0da67 | ||
|
|
7f2086488b | ||
|
|
3014fd8095 | ||
|
|
27885c8ac3 | ||
|
|
d6be0509ac | ||
|
|
1c85f5e857 | ||
|
|
abe5515aca | ||
|
|
6fba975490 | ||
|
|
2de6798f45 | ||
|
|
04fdfa2a35 | ||
|
|
8f3085290d | ||
|
|
0839fe45f5 | ||
|
|
18f4795fda | ||
|
|
55d9fa622a | ||
|
|
7dc723c764 | ||
|
|
5a63205972 | ||
|
|
a4ceeafb1e | ||
|
|
242e05cc0e | ||
|
|
065dddbe6e | ||
|
|
fa6825252b | ||
|
|
b06e48a444 | ||
|
|
97dbd40f07 | ||
|
|
bc23109f99 | ||
|
|
ecb9675e9c | ||
|
|
e1f9b9e7a4 | ||
|
|
067b485bb3 | ||
|
|
67a4e3074e | ||
|
|
010bc4e8c3 | ||
|
|
9de5e3253e | ||
|
|
e32622ac48 | ||
|
|
5e2371c2cb | ||
|
|
a6ce26ee87 | ||
|
|
2a72c126f1 | ||
|
|
36e1a5d379 | ||
|
|
c12eafa1db | ||
|
|
9e26d8755c | ||
|
|
90bd30e351 | ||
|
|
3fb5d5c4f3 | ||
|
|
9f3685e4d5 | ||
|
|
6565988952 | ||
|
|
10b7a0875b | ||
|
|
bb4b9f1a58 | ||
|
|
981e527560 | ||
|
|
80b2ee719c | ||
|
|
6103d6196f | ||
|
|
ed1a5bfded | ||
|
|
3909ce3350 | ||
|
|
d64cd0b8a4 | ||
|
|
676aa9f93f | ||
|
|
ebd48e2556 | ||
|
|
ad5871aae4 | ||
|
|
e327b1ca5b | ||
|
|
ad43ca11eb | ||
|
|
41ba76e2e2 | ||
|
|
15e773434e | ||
|
|
ed118f4e7a | ||
|
|
3c420f2e30 | ||
|
|
4e271d4f0e | ||
|
|
27f9b3cd0b | ||
|
|
1ed4abd347 | ||
|
|
f71dd1ed54 | ||
|
|
19828d3b06 | ||
|
|
d242e729f0 | ||
|
|
8cd0d5faa5 | ||
|
|
a741d892a9 | ||
|
|
9add3361e0 | ||
|
|
4aac70ab5f | ||
|
|
980aec714a | ||
|
|
af8ee5af0f | ||
|
|
32d9aa0cf2 | ||
|
|
7e49631912 | ||
|
|
43970d34aa | ||
|
|
abb3c40697 | ||
|
|
2757a41102 | ||
|
|
1f8bddaa5e | ||
|
|
4c3b7ca60f | ||
|
|
d9d83e5767 | ||
|
|
b4ebde47be | ||
|
|
ef9f76190d | ||
|
|
71b96efca0 | ||
|
|
7158e09b0e | ||
|
|
8ef125bed2 | ||
|
|
0b11fb2fd5 | ||
|
|
3871f3cf3d | ||
|
|
7c5d1ec0f6 | ||
|
|
b507b08e34 | ||
|
|
1b06090f72 | ||
|
|
5460c20ac3 | ||
|
|
2ccec607a0 | ||
|
|
8a99fcf188 | ||
|
|
1e2489ca76 | ||
|
|
89793d2d62 | ||
|
|
11a1af89f4 | ||
|
|
e24ddb804d | ||
|
|
3524d365be | ||
|
|
2b3b9d037c | ||
|
|
5140cd9d7f | ||
|
|
2df9437b39 | ||
|
|
db440b8a14 | ||
|
|
c3dd70bc99 | ||
|
|
223e783bbc | ||
|
|
ca086dbf16 | ||
|
|
523422cf6c | ||
|
|
2dc310dcbc | ||
|
|
c092cd2921 | ||
|
|
2b14ef76c9 | ||
|
|
fbbf10078f | ||
|
|
2315d423c4 | ||
|
|
9a43465ebf | ||
|
|
fc1444763d | ||
|
|
804bf879ed | ||
|
|
ad44f09421 | ||
|
|
df2469468b | ||
|
|
f8f4fe11eb | ||
|
|
039a370add | ||
|
|
6feebc086d | ||
|
|
bc335c7d72 | ||
|
|
a6dd7254b2 | ||
|
|
7b1026c624 | ||
|
|
4758393cc1 | ||
|
|
52373a3a7d | ||
|
|
c30f9a2841 | ||
|
|
44d6f8f15c | ||
|
|
5ada12f989 | ||
|
|
d213045168 | ||
|
|
d83478239e | ||
|
|
3869955357 | ||
|
|
345d37edf8 | ||
|
|
2788ef28cf | ||
|
|
0d5c1bb3df | ||
|
|
c3d505cdad | ||
|
|
90854e1dd4 | ||
|
|
f96e3b04be | ||
|
|
44449e26a0 | ||
|
|
ddc88fd360 | ||
|
|
fedec450cb | ||
|
|
04ea742830 | ||
|
|
5a5c860cef | ||
|
|
55d06a43f8 | ||
|
|
71eecd6e7b | ||
|
|
6f3019f84b | ||
|
|
e95d3126b2 | ||
|
|
5da265bf0b | ||
|
|
2ce9c43b8c | ||
|
|
740b2f206c | ||
|
|
af622bc7e7 | ||
|
|
7816b50b27 | ||
|
|
4cb7a909f7 | ||
|
|
0fac88e171 | ||
|
|
b4ab9d9650 | ||
|
|
731db13c14 | ||
|
|
414a1ad4d2 | ||
|
|
16055fe96e | ||
|
|
6140c398f0 | ||
|
|
bd02923616 | ||
|
|
6021815fd3 | ||
|
|
8c4aba5479 | ||
|
|
2428b22171 | ||
|
|
a3d30211f6 | ||
|
|
730300d211 | ||
|
|
aaca31276b | ||
|
|
53fb927e36 | ||
|
|
fb5aa0313e | ||
|
|
9b41eecbf1 | ||
|
|
ae461b1caf | ||
|
|
57e36d6710 | ||
|
|
a7c4f09c5b | ||
|
|
554ef16e49 | ||
|
|
082321f860 | ||
|
|
df4f7b8c9e | ||
|
|
3f1742f074 | ||
|
|
4560d5c2d5 | ||
|
|
0ca12d275c | ||
|
|
df9e834309 | ||
|
|
ab1c0bb129 | ||
|
|
5070e4c950 | ||
|
|
c13526ccad | ||
|
|
46e16a6c69 | ||
|
|
53983933dc | ||
|
|
09f3ca39a1 | ||
|
|
42b4c91f35 | ||
|
|
37120776be | ||
|
|
d67bcb66c9 | ||
|
|
acf6e72f35 | ||
|
|
0964b271f5 | ||
|
|
1f7998fc44 | ||
|
|
7ac59f7bd0 | ||
|
|
bf238640af | ||
|
|
6e638cadb7 | ||
|
|
ab0759f441 | ||
|
|
123ec5c78a | ||
|
|
d262c67832 | ||
|
|
fcb9a2838d | ||
|
|
82d1672741 | ||
|
|
657a8b6094 | ||
|
|
165f7e0720 | ||
|
|
91aba84c29 | ||
|
|
02c4ac1bc7 | ||
|
|
c529959027 | ||
|
|
e875d1a5d7 | ||
|
|
d280505b9f | ||
|
|
fdd9d0000b | ||
|
|
74c793eedf | ||
|
|
bd8976c620 | ||
|
|
a3e2563f9b | ||
|
|
a310e3d8fa | ||
|
|
ae9da35af1 | ||
|
|
b45837ff5c | ||
|
|
3dd9ab8a29 | ||
|
|
5ebd2f6b8e | ||
|
|
977043ac92 | ||
|
|
4c6182b79c | ||
|
|
01125e092c | ||
|
|
99ef447a52 | ||
|
|
4722ff9b97 | ||
|
|
23a19df79a | ||
|
|
97b86c6faa | ||
|
|
e6296f20b9 | ||
|
|
0ee9dcc255 | ||
|
|
c7b4e2c49d |
27
.githooks/pre-commit
Executable file
27
.githooks/pre-commit
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# DISABLED for now
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
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
|
||||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [avengemedia]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: danklinux
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -9,23 +9,25 @@ 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
|
- 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
|
- 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. -->
|
- 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
|
## Compositor
|
||||||
|
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
- [ ] Other (specify)
|
- [ ] Other (specify)
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
<!-- Arch, Fedora, Debian, etc. -->
|
||||||
|
|
||||||
|
## dms version
|
||||||
|
|
||||||
|
<!-- Output of dms version command -->
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- Brief description of the issue -->
|
<!-- Brief description of the issue -->
|
||||||
@@ -45,6 +47,14 @@ assignees: ""
|
|||||||
## Error Messages/Logs
|
## Error Messages/Logs
|
||||||
|
|
||||||
<!-- Please include any error messages, stack traces, or relevant logs -->
|
<!-- Please include any error messages, stack traces, or relevant logs -->
|
||||||
|
<!-- you can get a log file with the following steps:
|
||||||
|
dms kill
|
||||||
|
mkdir ~/dms_logs
|
||||||
|
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
|
||||||
|
|
||||||
|
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
```
|
```
|
||||||
Paste error messages or logs here
|
Paste error messages or logs here
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -21,6 +21,8 @@ Is this feature specific to one compositor?
|
|||||||
- [ ] All compositors
|
- [ ] All compositors
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
|
|
||||||
## Proposed Solution
|
## Proposed Solution
|
||||||
|
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/support_request.md
vendored
10
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -10,8 +10,18 @@ assignees: ""
|
|||||||
|
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
- [ ] other
|
- [ ] other
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
<!-- Arch, Fedora, Debian, etc. -->
|
||||||
|
|
||||||
|
## dms version
|
||||||
|
|
||||||
|
<!-- Output of dms version command -->
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- Brief description of the support needed -->
|
<!-- Brief description of the support needed -->
|
||||||
|
|||||||
302
.github/workflows/copr-release.yml
vendored
Normal file
302
.github/workflows/copr-release.yml
vendored
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
name: DMS Copr Stable Release (Manual)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-upload:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||||
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
|
echo "Using manual version: $VERSION"
|
||||||
|
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/DankMaterialShell
|
||||||
|
|
||||||
|
%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/DankMaterialShell/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 -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
|
%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
|
||||||
42
.github/workflows/go-ci.yml
vendored
Normal file
42
.github/workflows/go-ci.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Go CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
paths:
|
||||||
|
- 'backend/**'
|
||||||
|
- '.github/workflows/go-ci.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: backend
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./backend/go.mod
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
gofmt -s -l .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: Build dms
|
||||||
|
run: go build -v ./cmd/dms
|
||||||
|
|
||||||
|
- name: Build dankinstall
|
||||||
|
run: go build -v ./cmd/dankinstall
|
||||||
605
.github/workflows/release.yml
vendored
605
.github/workflows/release.yml
vendored
@@ -1,59 +1,608 @@
|
|||||||
name: Create Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref_name }}
|
group: release-${{ github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_release:
|
build-backend:
|
||||||
name: 📦 Create GitHub Release
|
runs-on: ubuntu-latest
|
||||||
runs-on: ubuntu-24.04
|
strategy:
|
||||||
steps:
|
matrix:
|
||||||
- uses: actions/checkout@v4
|
arch: [amd64, arm64]
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Fetch full history for changelog generation
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: backend
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./backend/go.mod
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- 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=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-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=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-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=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-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: backend-assets-${{ matrix.arch }}
|
||||||
|
path: |
|
||||||
|
backend/dankinstall-${{ matrix.arch }}.gz
|
||||||
|
backend/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
|
backend/dms-${{ matrix.arch }}.gz
|
||||||
|
backend/dms-${{ matrix.arch }}.gz.sha256
|
||||||
|
backend/dms-distropkg-${{ matrix.arch }}.gz
|
||||||
|
backend/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: backend-assets-${{ matrix.arch }}
|
||||||
|
path: |
|
||||||
|
backend/dankinstall-${{ matrix.arch }}.gz
|
||||||
|
backend/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
|
backend/dms-${{ matrix.arch }}.gz
|
||||||
|
backend/dms-${{ matrix.arch }}.gz.sha256
|
||||||
|
backend/dms-distropkg-${{ matrix.arch }}.gz
|
||||||
|
backend/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||||
|
backend/completion.bash
|
||||||
|
backend/completion.fish
|
||||||
|
backend/completion.zsh
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
update-versions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Update VERSION and flake.nix
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
version="${GITHUB_REF#refs/tags/}"
|
||||||
|
version_no_v="${version#v}"
|
||||||
|
echo "Updating to version: $version"
|
||||||
|
|
||||||
|
# Update VERSION file in quickshell/
|
||||||
|
echo "${version}" > quickshell/VERSION
|
||||||
|
|
||||||
|
# Update version in backend/flake.nix
|
||||||
|
sed -i "s/version = \"[^\"]*\"/version = \"$version_no_v\"/" backend/flake.nix
|
||||||
|
|
||||||
|
git add quickshell/VERSION backend/flake.nix
|
||||||
|
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git commit -m "chore: bump version to $version"
|
||||||
|
git push origin HEAD:master || git push origin HEAD:main
|
||||||
|
echo "Pushed version updates to master"
|
||||||
|
else
|
||||||
|
echo "No version changes needed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs: build-backend
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download backend artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: backend-assets-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: ./_backend_assets
|
||||||
|
|
||||||
# Generate changelog
|
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
run: |
|
run: |
|
||||||
# Get the previous tag
|
set -e
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
||||||
|
|
||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
echo "No previous tag found, using all commits"
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" | head -50)
|
|
||||||
else
|
else
|
||||||
echo "Generating changelog from $PREVIOUS_TAG to HEAD"
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD)
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create the changelog with proper formatting
|
cat > RELEASE_BODY.md << 'EOF'
|
||||||
cat > CHANGELOG.md << EOF
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://install.danklinux.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
### Complete Packages
|
||||||
|
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
|
||||||
|
### Individual Components
|
||||||
|
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||||
|
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||||
|
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||||
|
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||||
|
- **`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
|
||||||
|
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
||||||
|
|
||||||
|
**Installation:** Extract the `dms-full-*.tar.gz` package for your architecture and follow the `INSTALL.md` instructions inside.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >> RELEASE_BODY.md << EOF
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|
||||||
$CHANGELOG
|
$CHANGELOG
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${{ github.ref_name }}
|
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${TAG}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Set output for use in release step
|
|
||||||
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
||||||
cat CHANGELOG.md >> $GITHUB_OUTPUT
|
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Create GitHub Release
|
- name: Prepare release assets
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
mkdir -p _release_assets
|
||||||
|
|
||||||
|
# Copy backend binaries and rename dms-*.gz to dms-cli-*.gz
|
||||||
|
for file in _backend_assets/dms-*.gz*; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
basename=$(basename "$file")
|
||||||
|
if [[ "$basename" == dms-distropkg-* ]]; then
|
||||||
|
cp "$file" "_release_assets/$basename"
|
||||||
|
else
|
||||||
|
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
|
||||||
|
cp "$file" "_release_assets/$newname"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Copy dankinstall binaries
|
||||||
|
cp _backend_assets/dankinstall-*.gz* _release_assets/
|
||||||
|
|
||||||
|
# Copy completions
|
||||||
|
cp _backend_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create QML source package (exclude build artifacts and git files)
|
||||||
|
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||||
|
(cd quickshell && tar --exclude='.git' \
|
||||||
|
--exclude='.github' \
|
||||||
|
--exclude='*.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
|
||||||
|
|
||||||
|
# Add CLI binaries
|
||||||
|
if [ -f "_backend_assets/dms-${arch}.gz" ]; then
|
||||||
|
gunzip -c "_backend_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||||
|
chmod +x _temp_full/bin/dms
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "_backend_assets/dms-distropkg-${arch}.gz" ]; then
|
||||||
|
gunzip -c "_backend_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||||
|
chmod +x _temp_full/bin/dms-distropkg
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add shell completions
|
||||||
|
for completion in _backend_assets/completion.*; do
|
||||||
|
if [ -f "$completion" ]; then
|
||||||
|
cp "$completion" _temp_full/completions/
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create installation guide
|
||||||
|
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
|
||||||
|
# DankMaterialShell Installation
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Wayland compositor (niri or Hyprland recommended)
|
||||||
|
- Quickshell framework
|
||||||
|
- Qt6
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
1. **Install quickshell assets:**
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/quickshell
|
||||||
|
cp -r dms ~/.config/quickshell/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install the DMS CLI binaries:**
|
||||||
|
```bash
|
||||||
|
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install shell completions (optional):**
|
||||||
|
```bash
|
||||||
|
# Bash
|
||||||
|
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
||||||
|
|
||||||
|
# Fish
|
||||||
|
sudo install -m 644 completions/completion.fish /usr/share/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
|
# Zsh
|
||||||
|
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start the shell:**
|
||||||
|
```bash
|
||||||
|
dms run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Settings are stored in `~/.config/DankMaterialShell/settings.json`
|
||||||
|
- Plugins go in `~/.config/DankMaterialShell/plugins/`
|
||||||
|
- See the documentation in the `dms/` directory for more details
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- Run with verbose output: `quickshell -v -p ~/.config/quickshell/dms`
|
||||||
|
- Check logs in `~/.local/state/DankMaterialShell/`
|
||||||
|
- Ensure all dependencies are installed
|
||||||
|
EOFINSTALL
|
||||||
|
|
||||||
|
# Create the full package
|
||||||
|
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
||||||
|
|
||||||
|
# Generate checksum
|
||||||
|
(cd _release_assets && sha256sum "dms-full-${arch}.tar.gz" > "dms-full-${arch}.tar.gz.sha256")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf _temp_full
|
||||||
|
done
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: comnoco/create-release-action@v2.0.5
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
copr-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
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}
|
||||||
|
|
||||||
|
- name: Download release assets
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
cd ~/rpmbuild/SOURCES
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
- 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/DankMaterialShell
|
||||||
|
|
||||||
|
%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
|
||||||
|
case "%{_arch}" in
|
||||||
|
x86_64)
|
||||||
|
ARCH_SUFFIX="amd64"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
ARCH_SUFFIX="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: %{_arch}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/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
|
||||||
|
|
||||||
|
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
|
||||||
|
echo "Failed to download dgop for architecture %{_arch}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
|
||||||
|
chmod +x %{_builddir}/dgop
|
||||||
|
|
||||||
|
%build
|
||||||
|
|
||||||
|
%install
|
||||||
|
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
|
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
||||||
|
|
||||||
|
install -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 -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
|
%posttrans
|
||||||
|
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Build SRPM
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
cd ~/rpmbuild/SPECS
|
||||||
|
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"
|
||||||
|
|
||||||
|
- name: Upload SRPM artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
release_name: Release ${{ github.ref_name }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
body: ${{ steps.changelog.outputs.changelog }}
|
retention-days: 90
|
||||||
draft: false
|
|
||||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
- 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
|
||||||
|
|
||||||
|
- name: Upload to Copr
|
||||||
|
run: |
|
||||||
|
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
echo "Uploading SRPM to avengemedia/dms..."
|
||||||
|
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: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||||
|
fi
|
||||||
|
|||||||
90
.github/workflows/update-vendor-hash.yml
vendored
Normal file
90
.github/workflows/update-vendor-hash.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
name: Update Vendor Hash
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "backend/go.mod"
|
||||||
|
- "backend/go.sum"
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-vendor-hash:
|
||||||
|
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: Update vendorHash in backend/flake.nix
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Try to build and capture the expected hash from error message
|
||||||
|
echo "Attempting nix build to get new vendorHash..."
|
||||||
|
cd backend
|
||||||
|
if output=$(nix build .#dms-cli 2>&1); then
|
||||||
|
echo "Build succeeded, no hash update needed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the expected hash from the error message
|
||||||
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
||||||
|
|
||||||
|
if [ -z "$new_hash" ]; then
|
||||||
|
echo "Could not extract new vendorHash from build output"
|
||||||
|
echo "Build output:"
|
||||||
|
echo "$output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "New vendorHash: $new_hash"
|
||||||
|
|
||||||
|
# Get current hash from flake.nix
|
||||||
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
|
echo "Current vendorHash: $current_hash"
|
||||||
|
|
||||||
|
if [ "$current_hash" = "$new_hash" ]; then
|
||||||
|
echo "vendorHash is already up to date"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update the hash in flake.nix
|
||||||
|
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||||
|
|
||||||
|
# Verify the build works with the new hash
|
||||||
|
echo "Verifying build with new vendorHash..."
|
||||||
|
nix build .#dms-cli
|
||||||
|
|
||||||
|
echo "vendorHash updated successfully!"
|
||||||
|
|
||||||
|
- name: Commit and push vendorHash update
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! git diff --quiet backend/flake.nix; then
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
git add backend/flake.nix
|
||||||
|
git commit -m "flake: update vendorHash for go.mod changes"
|
||||||
|
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if git push; then
|
||||||
|
echo "Successfully pushed vendorHash update"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Push attempt $attempt failed, pulling and retrying..."
|
||||||
|
git pull --rebase
|
||||||
|
sleep $((attempt*2))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Failed to push after retries" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No changes to backend/flake.nix"
|
||||||
|
fi
|
||||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -101,4 +101,39 @@ go.work.sum
|
|||||||
|
|
||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Code coverage profiles and other test artifacts
|
||||||
|
*.out
|
||||||
|
coverage.*
|
||||||
|
*.coverprofile
|
||||||
|
profile.cov
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Editor/IDE
|
||||||
|
# .idea/
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
bin/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Contributions are welcome and encourages.
|
Contributions are welcome and encouraged.
|
||||||
|
|
||||||
## Formatting
|
## Formatting
|
||||||
|
|
||||||
@@ -27,4 +27,4 @@ Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you m
|
|||||||
|
|
||||||
## Pull request
|
## Pull request
|
||||||
|
|
||||||
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
|
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
853
Common/Theme.qml
853
Common/Theme.qml
@@ -1,853 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.UPower
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import "StockThemes.js" as StockThemes
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
|
|
||||||
|
|
||||||
readonly property real popupDistance: 4
|
|
||||||
|
|
||||||
property string currentTheme: "blue"
|
|
||||||
property string currentThemeCategory: "generic"
|
|
||||||
property bool isLightMode: false
|
|
||||||
|
|
||||||
readonly property string dynamic: "dynamic"
|
|
||||||
readonly property string custom : "custom"
|
|
||||||
|
|
||||||
readonly property string homeDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation))
|
|
||||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
|
||||||
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "")
|
|
||||||
readonly property string wallpaperPath: {
|
|
||||||
if (typeof SessionData === "undefined") return ""
|
|
||||||
|
|
||||||
if (SessionData.perMonitorWallpaper) {
|
|
||||||
// Use first monitor's wallpaper for dynamic theming
|
|
||||||
var screens = Quickshell.screens
|
|
||||||
if (screens.length > 0) {
|
|
||||||
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
|
|
||||||
var wallpaperPath = firstMonitorWallpaper || SessionData.wallpaperPath
|
|
||||||
|
|
||||||
if (wallpaperPath && wallpaperPath.startsWith("we:")) {
|
|
||||||
return stateDir + "/we_screenshots/" + wallpaperPath.substring(3) + ".jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
return wallpaperPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var wallpaperPath = SessionData.wallpaperPath
|
|
||||||
var screens = Quickshell.screens
|
|
||||||
if (screens.length > 0 && wallpaperPath && wallpaperPath.startsWith("we:")) {
|
|
||||||
return stateDir + "/we_screenshots/" + wallpaperPath.substring(3) + ".jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
return wallpaperPath
|
|
||||||
}
|
|
||||||
readonly property string rawWallpaperPath: {
|
|
||||||
if (typeof SessionData === "undefined") return ""
|
|
||||||
|
|
||||||
if (SessionData.perMonitorWallpaper) {
|
|
||||||
// Use first monitor's wallpaper for dynamic theming
|
|
||||||
var screens = Quickshell.screens
|
|
||||||
if (screens.length > 0) {
|
|
||||||
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
|
|
||||||
return firstMonitorWallpaper || SessionData.wallpaperPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SessionData.wallpaperPath
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool matugenAvailable: false
|
|
||||||
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
|
|
||||||
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
|
|
||||||
property var workerRunning: false
|
|
||||||
property var matugenColors: ({})
|
|
||||||
property int colorUpdateTrigger: 0
|
|
||||||
property var customThemeData: null
|
|
||||||
|
|
||||||
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()) + "/dankshell"
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
Quickshell.execDetached(["mkdir", "-p", stateDir])
|
|
||||||
matugenCheck.running = true
|
|
||||||
if (typeof SessionData !== "undefined")
|
|
||||||
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
|
|
||||||
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
|
||||||
switchTheme(SettingsData.currentThemeName, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMatugenColor(path, fallback) {
|
|
||||||
colorUpdateTrigger
|
|
||||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
|
||||||
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]
|
|
||||||
for (const part of path.split(".")) {
|
|
||||||
if (!cur || typeof cur !== "object" || !(part in cur))
|
|
||||||
return fallback
|
|
||||||
cur = cur[part]
|
|
||||||
}
|
|
||||||
return cur || fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var currentThemeData: {
|
|
||||||
if (currentTheme === "custom") {
|
|
||||||
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
|
|
||||||
} else if (currentTheme === dynamic) {
|
|
||||||
return {
|
|
||||||
"primary": getMatugenColor("primary", "#42a5f5"),
|
|
||||||
"primaryText": getMatugenColor("on_primary", "#ffffff"),
|
|
||||||
"primaryContainer": getMatugenColor("primary_container", "#1976d2"),
|
|
||||||
"secondary": getMatugenColor("secondary", "#8ab4f8"),
|
|
||||||
"surface": getMatugenColor("surface", "#1a1c1e"),
|
|
||||||
"surfaceText": getMatugenColor("on_background", "#e3e8ef"),
|
|
||||||
"surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
|
|
||||||
"surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"),
|
|
||||||
"surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"),
|
|
||||||
"background": getMatugenColor("background", "#1a1c1e"),
|
|
||||||
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
|
|
||||||
"outline": getMatugenColor("outline", "#8e918f"),
|
|
||||||
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
|
|
||||||
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
|
|
||||||
"surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
|
|
||||||
"error": "#F2B8B5",
|
|
||||||
"warning": "#FF9800",
|
|
||||||
"info": "#2196F3",
|
|
||||||
"success": "#4CAF50"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return StockThemes.getThemeByName(currentTheme, isLightMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var availableMatugenSchemes: [
|
|
||||||
({ "value": "scheme-tonal-spot", "label": "Tonal Spot", "description": "Balanced palette with focused accents (default)." }),
|
|
||||||
({ "value": "scheme-content", "label": "Content", "description": "Derives colors that closely match the underlying image." }),
|
|
||||||
({ "value": "scheme-expressive", "label": "Expressive", "description": "Vibrant palette with playful saturation." }),
|
|
||||||
({ "value": "scheme-fidelity", "label": "Fidelity", "description": "High-fidelity palette that preserves source hues." }),
|
|
||||||
({ "value": "scheme-fruit-salad", "label": "Fruit Salad", "description": "Colorful mix of bright contrasting accents." }),
|
|
||||||
({ "value": "scheme-monochrome", "label": "Monochrome", "description": "Minimal palette built around a single hue." }),
|
|
||||||
({ "value": "scheme-neutral", "label": "Neutral", "description": "Muted palette with subdued, calming tones." }),
|
|
||||||
({ "value": "scheme-rainbow", "label": "Rainbow", "description": "Diverse palette spanning the full spectrum." })
|
|
||||||
]
|
|
||||||
|
|
||||||
function getMatugenScheme(value) {
|
|
||||||
const schemes = availableMatugenSchemes
|
|
||||||
for (let i = 0; i < schemes.length; i++) {
|
|
||||||
if (schemes[i].value === value)
|
|
||||||
return schemes[i]
|
|
||||||
}
|
|
||||||
return schemes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
property color primary: currentThemeData.primary
|
|
||||||
property color primaryText: currentThemeData.primaryText
|
|
||||||
property color primaryContainer: currentThemeData.primaryContainer
|
|
||||||
property color secondary: currentThemeData.secondary
|
|
||||||
property color surface: {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
|
||||||
return currentThemeData.background
|
|
||||||
}
|
|
||||||
return currentThemeData.surface
|
|
||||||
}
|
|
||||||
property color surfaceText: currentThemeData.surfaceText
|
|
||||||
property color surfaceVariant: currentThemeData.surfaceVariant
|
|
||||||
property color surfaceVariantText: currentThemeData.surfaceVariantText
|
|
||||||
property color surfaceTint: currentThemeData.surfaceTint
|
|
||||||
property color background: currentThemeData.background
|
|
||||||
property color backgroundText: currentThemeData.backgroundText
|
|
||||||
property color outline: currentThemeData.outline
|
|
||||||
property color outlineVariant: currentThemeData.outlineVariant || Qt.rgba(outline.r, outline.g, outline.b, 0.6)
|
|
||||||
property color surfaceContainer: {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
|
||||||
return currentThemeData.surface
|
|
||||||
}
|
|
||||||
return currentThemeData.surfaceContainer
|
|
||||||
}
|
|
||||||
property color surfaceContainerHigh: {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
|
||||||
return currentThemeData.surfaceContainer
|
|
||||||
}
|
|
||||||
return currentThemeData.surfaceContainerHigh
|
|
||||||
}
|
|
||||||
property color surfaceContainerHighest: {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
|
||||||
return currentThemeData.surfaceContainerHigh
|
|
||||||
}
|
|
||||||
return currentThemeData.surfaceContainerHighest
|
|
||||||
}
|
|
||||||
|
|
||||||
property color onSurface: surfaceText
|
|
||||||
property color onSurfaceVariant: surfaceVariantText
|
|
||||||
property color onPrimary: primaryText
|
|
||||||
property color onSurface_12: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.12)
|
|
||||||
property color onSurface_38: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.38)
|
|
||||||
property color onSurfaceVariant_30: Qt.rgba(onSurfaceVariant.r, onSurfaceVariant.g, onSurfaceVariant.b, 0.30)
|
|
||||||
|
|
||||||
property color error: currentThemeData.error || "#F2B8B5"
|
|
||||||
property color warning: currentThemeData.warning || "#FF9800"
|
|
||||||
property color info: currentThemeData.info || "#2196F3"
|
|
||||||
property color tempWarning: "#ff9933"
|
|
||||||
property color tempDanger: "#ff5555"
|
|
||||||
property color success: currentThemeData.success || "#4CAF50"
|
|
||||||
|
|
||||||
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
|
|
||||||
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
|
|
||||||
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
|
|
||||||
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
|
|
||||||
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
|
|
||||||
|
|
||||||
property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08)
|
|
||||||
|
|
||||||
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
|
|
||||||
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
|
|
||||||
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
|
|
||||||
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
|
|
||||||
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
|
|
||||||
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
|
|
||||||
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
|
|
||||||
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
|
|
||||||
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
|
|
||||||
|
|
||||||
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
|
|
||||||
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
|
|
||||||
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
|
|
||||||
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
|
|
||||||
|
|
||||||
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
|
||||||
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
|
||||||
|
|
||||||
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
|
||||||
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
|
||||||
|
|
||||||
property int shorterDuration: 100
|
|
||||||
property int shortDuration: 150
|
|
||||||
property int mediumDuration: 300
|
|
||||||
property int longDuration: 500
|
|
||||||
property int extraLongDuration: 1000
|
|
||||||
property int standardEasing: Easing.OutCubic
|
|
||||||
property int emphasizedEasing: Easing.OutQuart
|
|
||||||
|
|
||||||
property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
|
|
||||||
property real spacingXS: 4
|
|
||||||
property real spacingS: 8
|
|
||||||
property real spacingM: 12
|
|
||||||
property real spacingL: 16
|
|
||||||
property real spacingXL: 24
|
|
||||||
property real fontSizeSmall: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 12
|
|
||||||
property real fontSizeMedium: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 14
|
|
||||||
property real fontSizeLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 16
|
|
||||||
property real fontSizeXLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 20
|
|
||||||
property real barHeight: 48
|
|
||||||
property real iconSize: 24
|
|
||||||
property real iconSizeSmall: 16
|
|
||||||
property real iconSizeLarge: 32
|
|
||||||
|
|
||||||
property real panelTransparency: 0.85
|
|
||||||
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85
|
|
||||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92
|
|
||||||
|
|
||||||
function screenTransition() {
|
|
||||||
CompositorService.isNiri && NiriService.doScreenTransition()
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
|
||||||
if (enableTransition) {
|
|
||||||
screenTransition()
|
|
||||||
}
|
|
||||||
if (themeName === dynamic) {
|
|
||||||
currentTheme = dynamic
|
|
||||||
currentThemeCategory = dynamic
|
|
||||||
} else if (themeName === custom) {
|
|
||||||
currentTheme = custom
|
|
||||||
currentThemeCategory = custom
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
|
|
||||||
loadCustomThemeFromFile(SettingsData.customThemeFile)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentTheme = themeName
|
|
||||||
// Determine category based on theme name
|
|
||||||
if (StockThemes.isCatppuccinVariant(themeName)) {
|
|
||||||
currentThemeCategory = "catppuccin"
|
|
||||||
} else {
|
|
||||||
currentThemeCategory = "generic"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (savePrefs && typeof SettingsData !== "undefined")
|
|
||||||
SettingsData.setTheme(currentTheme)
|
|
||||||
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLightMode(light, savePrefs = true) {
|
|
||||||
screenTransition()
|
|
||||||
isLightMode = light
|
|
||||||
if (savePrefs && typeof SessionData !== "undefined")
|
|
||||||
SessionData.setLightMode(isLightMode)
|
|
||||||
PortalService.setLightMode(isLightMode)
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleLightMode(savePrefs = true) {
|
|
||||||
setLightMode(!isLightMode, savePrefs)
|
|
||||||
}
|
|
||||||
|
|
||||||
function forceGenerateSystemThemes() {
|
|
||||||
screenTransition()
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAvailableThemes() {
|
|
||||||
return StockThemes.getAllThemeNames()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThemeDisplayName(themeName) {
|
|
||||||
const themeData = StockThemes.getThemeByName(themeName, isLightMode)
|
|
||||||
return themeData.name
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThemeColors(themeName) {
|
|
||||||
if (themeName === "custom" && customThemeData) {
|
|
||||||
return customThemeData
|
|
||||||
}
|
|
||||||
return StockThemes.getThemeByName(themeName, isLightMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchThemeCategory(category, defaultTheme) {
|
|
||||||
currentThemeCategory = category
|
|
||||||
switchTheme(defaultTheme, true, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCatppuccinColor(variantName) {
|
|
||||||
const catColors = {
|
|
||||||
"cat-rosewater": "#f5e0dc", "cat-flamingo": "#f2cdcd", "cat-pink": "#f5c2e7", "cat-mauve": "#cba6f7",
|
|
||||||
"cat-red": "#f38ba8", "cat-maroon": "#eba0ac", "cat-peach": "#fab387", "cat-yellow": "#f9e2af",
|
|
||||||
"cat-green": "#a6e3a1", "cat-teal": "#94e2d5", "cat-sky": "#89dceb", "cat-sapphire": "#74c7ec",
|
|
||||||
"cat-blue": "#89b4fa", "cat-lavender": "#b4befe"
|
|
||||||
}
|
|
||||||
return catColors[variantName] || "#cba6f7"
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCatppuccinVariantName(variantName) {
|
|
||||||
const catNames = {
|
|
||||||
"cat-rosewater": "Rosewater", "cat-flamingo": "Flamingo", "cat-pink": "Pink", "cat-mauve": "Mauve",
|
|
||||||
"cat-red": "Red", "cat-maroon": "Maroon", "cat-peach": "Peach", "cat-yellow": "Yellow",
|
|
||||||
"cat-green": "Green", "cat-teal": "Teal", "cat-sky": "Sky", "cat-sapphire": "Sapphire",
|
|
||||||
"cat-blue": "Blue", "cat-lavender": "Lavender"
|
|
||||||
}
|
|
||||||
return catNames[variantName] || "Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCustomTheme(themeData) {
|
|
||||||
screenTransition()
|
|
||||||
if (themeData.dark || themeData.light) {
|
|
||||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
|
||||||
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
|
|
||||||
customThemeData = selectedTheme
|
|
||||||
} else {
|
|
||||||
customThemeData = themeData
|
|
||||||
}
|
|
||||||
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCustomThemeFromFile(filePath) {
|
|
||||||
customThemeFileView.path = filePath
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias availableThemeNames: root._availableThemeNames
|
|
||||||
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
|
|
||||||
property string currentThemeName: currentTheme
|
|
||||||
|
|
||||||
function popupBackground() {
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
function contentBackground() {
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
function panelBackground() {
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
|
|
||||||
|
|
||||||
property var widgetBaseBackgroundColor: {
|
|
||||||
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
|
|
||||||
switch (colorMode) {
|
|
||||||
case "s":
|
|
||||||
return surface
|
|
||||||
case "sc":
|
|
||||||
return surfaceContainer
|
|
||||||
case "sch":
|
|
||||||
return surfaceContainerHigh
|
|
||||||
case "sth":
|
|
||||||
default:
|
|
||||||
return surfaceTextHover
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var widgetBaseHoverColor: {
|
|
||||||
const baseColor = widgetBaseBackgroundColor
|
|
||||||
const factor = 1.2
|
|
||||||
return isLightMode ? Qt.darker(baseColor, factor) : Qt.lighter(baseColor, factor)
|
|
||||||
}
|
|
||||||
|
|
||||||
property var widgetBackground: {
|
|
||||||
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
|
|
||||||
switch (colorMode) {
|
|
||||||
case "s":
|
|
||||||
return Qt.rgba(surface.r, surface.g, surface.b, widgetTransparency)
|
|
||||||
case "sc":
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
|
||||||
case "sch":
|
|
||||||
return Qt.rgba(surfaceContainerHigh.r, surfaceContainerHigh.g, surfaceContainerHigh.b, widgetTransparency)
|
|
||||||
case "sth":
|
|
||||||
default:
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPopupBackgroundAlpha() {
|
|
||||||
return popupTransparency
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContentBackgroundAlpha() {
|
|
||||||
return popupTransparency
|
|
||||||
}
|
|
||||||
|
|
||||||
function isColorDark(c) {
|
|
||||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBatteryIcon(level, isCharging, batteryAvailable) {
|
|
||||||
if (!batteryAvailable)
|
|
||||||
return _getBatteryPowerProfileIcon()
|
|
||||||
|
|
||||||
if (isCharging) {
|
|
||||||
if (level >= 90)
|
|
||||||
return "battery_charging_full"
|
|
||||||
if (level >= 80)
|
|
||||||
return "battery_charging_90"
|
|
||||||
if (level >= 60)
|
|
||||||
return "battery_charging_80"
|
|
||||||
if (level >= 50)
|
|
||||||
return "battery_charging_60"
|
|
||||||
if (level >= 30)
|
|
||||||
return "battery_charging_50"
|
|
||||||
if (level >= 20)
|
|
||||||
return "battery_charging_30"
|
|
||||||
return "battery_charging_20"
|
|
||||||
} else {
|
|
||||||
if (level >= 95)
|
|
||||||
return "battery_full"
|
|
||||||
if (level >= 85)
|
|
||||||
return "battery_6_bar"
|
|
||||||
if (level >= 70)
|
|
||||||
return "battery_5_bar"
|
|
||||||
if (level >= 55)
|
|
||||||
return "battery_4_bar"
|
|
||||||
if (level >= 40)
|
|
||||||
return "battery_3_bar"
|
|
||||||
if (level >= 25)
|
|
||||||
return "battery_2_bar"
|
|
||||||
if (level >= 10)
|
|
||||||
return "battery_1_bar"
|
|
||||||
return "battery_alert"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getBatteryPowerProfileIcon() {
|
|
||||||
if (typeof PowerProfiles === "undefined")
|
|
||||||
return "balance"
|
|
||||||
|
|
||||||
switch (PowerProfiles.profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "energy_savings_leaf"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "rocket_launch"
|
|
||||||
default:
|
|
||||||
return "balance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPowerProfileIcon(profile) {
|
|
||||||
switch (profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "battery_saver"
|
|
||||||
case PowerProfile.Balanced:
|
|
||||||
return "battery_std"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "flash_on"
|
|
||||||
default:
|
|
||||||
return "settings"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPowerProfileLabel(profile) {
|
|
||||||
switch (profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "Power Saver"
|
|
||||||
case PowerProfile.Balanced:
|
|
||||||
return "Balanced"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "Performance"
|
|
||||||
default:
|
|
||||||
return profile.charAt(0).toUpperCase() + profile.slice(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPowerProfileDescription(profile) {
|
|
||||||
switch (profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "Extend battery life"
|
|
||||||
case PowerProfile.Balanced:
|
|
||||||
return "Balance power and performance"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "Prioritize performance"
|
|
||||||
default:
|
|
||||||
return "Custom power profile"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onLightModeChanged() {
|
|
||||||
if (matugenColors && Object.keys(matugenColors).length > 0) {
|
|
||||||
colorUpdateTrigger++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTheme === "custom" && customThemeFileView.path) {
|
|
||||||
customThemeFileView.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType) {
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
console.warn("matugen not available or disabled - cannot set system theme")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
|
||||||
NiriService.suppressNextToast()
|
|
||||||
}
|
|
||||||
|
|
||||||
const desired = {
|
|
||||||
"kind": kind,
|
|
||||||
"value": value,
|
|
||||||
"mode": isLight ? "light" : "dark",
|
|
||||||
"iconTheme": iconTheme || "System Default",
|
|
||||||
"matugenType": matugenType || "scheme-tonal-spot",
|
|
||||||
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc"
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = JSON.stringify(desired)
|
|
||||||
const desiredPath = stateDir + "/matugen.desired.json"
|
|
||||||
|
|
||||||
Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`])
|
|
||||||
workerRunning = true
|
|
||||||
if (rawWallpaperPath.startsWith("we:")) {
|
|
||||||
console.log("calling matugen worker")
|
|
||||||
systemThemeGenerator.command = [
|
|
||||||
"sh", "-c",
|
|
||||||
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' --run`
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"]
|
|
||||||
}
|
|
||||||
systemThemeGenerator.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSystemThemesFromCurrentTheme() {
|
|
||||||
if (!matugenAvailable)
|
|
||||||
return
|
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
|
||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
|
||||||
|
|
||||||
if (currentTheme === dynamic) {
|
|
||||||
if (!wallpaperPath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
|
||||||
if (wallpaperPath.startsWith("#")) {
|
|
||||||
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
|
||||||
} else {
|
|
||||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let primaryColor
|
|
||||||
let matugenType
|
|
||||||
if (currentTheme === "custom") {
|
|
||||||
if (!customThemeData || !customThemeData.primary) {
|
|
||||||
console.warn("Custom theme data not available for system theme generation")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
primaryColor = customThemeData.primary
|
|
||||||
matugenType = customThemeData.matugen_type
|
|
||||||
} else {
|
|
||||||
primaryColor = currentThemeData.primary
|
|
||||||
matugenType = currentThemeData.matugen_type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!primaryColor) {
|
|
||||||
console.warn("No primary color available for theme:", currentTheme)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyGtkColors() {
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("matugen not available or disabled - cannot apply GTK colors")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
|
|
||||||
gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir]
|
|
||||||
gtkApplier.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyQtColors() {
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("matugen not available or disabled - cannot apply Qt colors")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
qtApplier.command = [shellDir + "/scripts/qt.sh", configDir]
|
|
||||||
qtApplier.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: matugenCheck
|
|
||||||
command: ["which", "matugen"]
|
|
||||||
onExited: code => {
|
|
||||||
matugenAvailable = (code === 0) && !envDisableMatugen
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
console.log("matugen not not available in path or disabled via DMS_DISABLE_MATUGEN")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
|
||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
|
||||||
|
|
||||||
if (currentTheme === dynamic) {
|
|
||||||
if (wallpaperPath) {
|
|
||||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
|
||||||
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
|
||||||
if (wallpaperPath.startsWith("#")) {
|
|
||||||
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
|
||||||
} else {
|
|
||||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let primaryColor
|
|
||||||
let matugenType
|
|
||||||
if (currentTheme === "custom") {
|
|
||||||
if (customThemeData && customThemeData.primary) {
|
|
||||||
primaryColor = customThemeData.primary
|
|
||||||
matugenType = customThemeData.matugen_type
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
primaryColor = currentThemeData.primary
|
|
||||||
matugenType = currentThemeData.matugen_type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (primaryColor) {
|
|
||||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
|
||||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ensureStateDir
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: systemThemeGenerator
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
workerRunning = false
|
|
||||||
|
|
||||||
if (exitCode === 2) {
|
|
||||||
// Exit code 2 means wallpaper/color not found - this is expected on first run
|
|
||||||
console.log("Theme worker: wallpaper/color not found, skipping theme generation")
|
|
||||||
} else if (exitCode !== 0) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Theme worker failed (" + exitCode + ")")
|
|
||||||
}
|
|
||||||
console.warn("Theme worker failed with exit code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: gtkApplier
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
id: gtkStdout
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
id: gtkStderr
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
|
|
||||||
ToastService.showInfo("GTK colors applied successfully")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Failed to apply GTK colors: " + gtkStderr.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: qtApplier
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
id: qtStdout
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
id: qtStderr
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showInfo("Qt colors applied successfully")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Failed to apply Qt colors: " + qtStderr.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: customThemeFileView
|
|
||||||
watchChanges: currentTheme === "custom"
|
|
||||||
|
|
||||||
function parseAndLoadTheme() {
|
|
||||||
try {
|
|
||||||
var themeData = JSON.parse(customThemeFileView.text())
|
|
||||||
loadCustomTheme(themeData)
|
|
||||||
} catch (e) {
|
|
||||||
ToastService.showError("Invalid JSON format: " + e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
parseAndLoadTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileChanged: {
|
|
||||||
customThemeFileView.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadFailed: function (error) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Failed to read theme file: " + error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: dynamicColorsFileView
|
|
||||||
path: stateDir + "/dms-colors.json"
|
|
||||||
watchChanges: currentTheme === dynamic
|
|
||||||
|
|
||||||
function parseAndLoadColors() {
|
|
||||||
try {
|
|
||||||
const colorsText = dynamicColorsFileView.text()
|
|
||||||
if (colorsText) {
|
|
||||||
root.matugenColors = JSON.parse(colorsText)
|
|
||||||
root.colorUpdateTrigger++
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.clearWallpaperError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.wallpaperErrorStatus = "error"
|
|
||||||
ToastService.showError("Dynamic colors parse error: " + e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (currentTheme === dynamic) {
|
|
||||||
parseAndLoadColors()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileChanged: {
|
|
||||||
if (currentTheme === dynamic) {
|
|
||||||
dynamicColorsFileView.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadFailed: function (error) {
|
|
||||||
if (currentTheme === dynamic && typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Failed to read dynamic colors: " + error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "theme"
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
root.toggleLightMode()
|
|
||||||
return root.isLightMode ? "light" : "dark"
|
|
||||||
}
|
|
||||||
|
|
||||||
function light(): string {
|
|
||||||
root.setLightMode(true)
|
|
||||||
return "light"
|
|
||||||
}
|
|
||||||
|
|
||||||
function dark(): string {
|
|
||||||
root.setLightMode(false)
|
|
||||||
return "dark"
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMode(): string {
|
|
||||||
return root.isLightMode ? "light" : "dark"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
695
LICENSE
695
LICENSE
@@ -1,674 +1,21 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
MIT License
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
Copyright (c) 2025 Avenge Media LLC
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this license document, but changing it is not allowed.
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
Preamble
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
The GNU General Public License is a free, copyleft license for
|
furnished to do so, subject to the following conditions:
|
||||||
software and other kinds of works.
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
The licenses for most software and other practical works are designed
|
copies or substantial portions of the Software.
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
share and change all versions of a program--to make sure it remains free
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
GNU General Public License for most of our software; it applies also to
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
any other work released this way by its authors. You can apply it to
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
your programs, too.
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Qt.labs.platform
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: colorPickerModal
|
|
||||||
|
|
||||||
signal colorSelected(color selectedColor)
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
colorDialog.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
colorDialog.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyColorToClipboard(colorValue) {
|
|
||||||
Quickshell.execDetached(["sh", "-c", `echo "${colorValue}" | wl-copy`])
|
|
||||||
ToastService.showInfo(`Color ${colorValue} copied to clipboard`)
|
|
||||||
console.log("Copied color to clipboard:", colorValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
ColorDialog {
|
|
||||||
id: colorDialog
|
|
||||||
title: "Color Picker - Select and copy color"
|
|
||||||
color: Theme.primary
|
|
||||||
|
|
||||||
onAccepted: {
|
|
||||||
const colorString = color.toString()
|
|
||||||
copyColorToClipboard(colorString)
|
|
||||||
colorSelected(color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "quickshell:modal"
|
|
||||||
|
|
||||||
property alias content: contentLoader.sourceComponent
|
|
||||||
property alias contentLoader: contentLoader
|
|
||||||
property real width: 400
|
|
||||||
property real height: 300
|
|
||||||
readonly property real screenWidth: screen ? screen.width : 1920
|
|
||||||
readonly property real screenHeight: screen ? screen.height : 1080
|
|
||||||
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.shorterDuration
|
|
||||||
property var animationEasing: Theme.emphasizedEasing
|
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
|
||||||
property color borderColor: Theme.outlineMedium
|
|
||||||
property real borderWidth: 1
|
|
||||||
property real cornerRadius: Theme.cornerRadius
|
|
||||||
property bool enableShadow: false
|
|
||||||
property alias modalFocusScope: focusScope
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property bool shouldHaveFocus: shouldBeVisible
|
|
||||||
property bool allowFocusOverride: false
|
|
||||||
property bool allowStacking: false
|
|
||||||
|
|
||||||
signal opened
|
|
||||||
signal dialogClosed
|
|
||||||
signal backgroundClicked
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
ModalManager.openModal(root)
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
focusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
shouldBeVisible = false
|
|
||||||
closeTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
close()
|
|
||||||
} else {
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: shouldBeVisible
|
|
||||||
color: "transparent"
|
|
||||||
WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under modal
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (root.visible) {
|
|
||||||
opened()
|
|
||||||
} else {
|
|
||||||
if (Qt.inputMethod) {
|
|
||||||
Qt.inputMethod.hide()
|
|
||||||
Qt.inputMethod.reset()
|
|
||||||
}
|
|
||||||
dialogClosed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: ModalManager
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
|
|
||||||
interval: animationDuration + 50
|
|
||||||
onTriggered: {
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: background
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "black"
|
|
||||||
opacity: root.showBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
|
||||||
visible: root.showBackground
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.closeOnBackgroundClick
|
|
||||||
onClicked: mouse => {
|
|
||||||
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
|
||||||
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
|
||||||
root.backgroundClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: root.animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contentContainer
|
|
||||||
|
|
||||||
width: root.width
|
|
||||||
height: root.height
|
|
||||||
anchors.centerIn: positioning === "center" ? parent : undefined
|
|
||||||
x: {
|
|
||||||
if (positioning === "top-right") {
|
|
||||||
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL)
|
|
||||||
} else if (positioning === "custom") {
|
|
||||||
return root.customPosition.x
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (positioning === "top-right") {
|
|
||||||
return Theme.barHeight + Theme.spacingXS
|
|
||||||
} else if (positioning === "custom") {
|
|
||||||
return root.customPosition.y
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
color: root.backgroundColor
|
|
||||||
radius: root.cornerRadius
|
|
||||||
border.color: root.borderColor
|
|
||||||
border.width: root.borderWidth
|
|
||||||
layer.enabled: root.enableShadow
|
|
||||||
opacity: root.shouldBeVisible ? 1 : 0
|
|
||||||
scale: root.animationType === "scale" ? (root.shouldBeVisible ? 1 : 0.9) : 1
|
|
||||||
transform: root.animationType === "slide" ? slideTransform : null
|
|
||||||
|
|
||||||
Translate {
|
|
||||||
id: slideTransform
|
|
||||||
|
|
||||||
x: root.shouldBeVisible ? 0 : 15
|
|
||||||
y: root.shouldBeVisible ? 0 : -30
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.visible
|
|
||||||
asynchronous: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: root.animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
enabled: root.animationType === "scale"
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: root.animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 8
|
|
||||||
shadowBlur: 1
|
|
||||||
shadowColor: Theme.shadowStrong
|
|
||||||
shadowOpacity: 0.3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: focusScope
|
|
||||||
|
|
||||||
objectName: "modalFocusScope"
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.visible // Only active when the modal is visible
|
|
||||||
focus: root.visible
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && shouldHaveFocus) {
|
|
||||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldHaveFocusChanged() {
|
|
||||||
if (shouldHaveFocus && visible) {
|
|
||||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,908 +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.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: fileBrowserModal
|
|
||||||
|
|
||||||
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
|
||||||
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" // "wallpaper" or "profile" for last path memory
|
|
||||||
property bool showHiddenFiles: false
|
|
||||||
property int selectedIndex: -1
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property bool backButtonFocused: false
|
|
||||||
property bool saveMode: false // Enable save functionality
|
|
||||||
property string defaultFileName: "" // Default filename for save mode
|
|
||||||
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 bool weAvailable: false
|
|
||||||
property string wePath: ""
|
|
||||||
property bool weMode: false
|
|
||||||
|
|
||||||
signal fileSelected(string path)
|
|
||||||
|
|
||||||
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 lastPath = browserType === "wallpaper" ? SessionData.wallpaperLastPath : browserType === "profile" ? SessionData.profileLastPath : ""
|
|
||||||
return (lastPath && lastPath !== "") ? lastPath : homeDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveLastPath(path) {
|
|
||||||
if (browserType === "wallpaper") {
|
|
||||||
SessionData.setWallpaperLastPath(path)
|
|
||||||
} else if (browserType === "profile") {
|
|
||||||
SessionData.setProfileLastPath(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) {
|
|
||||||
// Ensure the filePath has the correct file:// protocol format
|
|
||||||
var normalizedPath = filePath
|
|
||||||
if (!normalizedPath.startsWith("file://")) {
|
|
||||||
normalizedPath = "file://" + filePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file exists by looking through the folder model
|
|
||||||
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
|
|
||||||
Component.onCompleted: {
|
|
||||||
currentPath = getLastPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
function discoverWallpaperEngine() {
|
|
||||||
currentPathIndex = 0
|
|
||||||
checkNextPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkNextPath() {
|
|
||||||
if (currentPathIndex >= steamPaths.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const wePath = steamPaths[currentPathIndex]
|
|
||||||
const cleanPath = wePath.replace(/^file:\/\//, '')
|
|
||||||
weDiscoveryProcess.command = ["test", "-d", cleanPath]
|
|
||||||
weDiscoveryProcess.wePath = wePath
|
|
||||||
weDiscoveryProcess.running = true
|
|
||||||
}
|
|
||||||
width: 800
|
|
||||||
height: 600
|
|
||||||
enableShadow: true
|
|
||||||
visible: false
|
|
||||||
onBackgroundClicked: close()
|
|
||||||
onOpened: {
|
|
||||||
modalFocusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
|
||||||
keyboardController.handleKey(event)
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
currentPath = getLastPath()
|
|
||||||
selectedIndex = -1
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
backButtonFocused = false
|
|
||||||
if (browserType === "wallpaper" && !weAvailable) {
|
|
||||||
discoverWallpaperEngine()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onCurrentPathChanged: {
|
|
||||||
selectedFilePath = ""
|
|
||||||
selectedFileName = ""
|
|
||||||
selectedFileIsDir = false
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: keyboardController
|
|
||||||
|
|
||||||
property int totalItems: folderModel.count
|
|
||||||
property int gridColumns: 5
|
|
||||||
|
|
||||||
function handleKey(event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
close()
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// F10 toggles keyboard hints
|
|
||||||
if (event.key === Qt.Key_F10) {
|
|
||||||
showKeyboardHints = !showKeyboardHints
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// F1 or I key for file information
|
|
||||||
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
|
||||||
showFileInfo = !showFileInfo
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Alt+Left or Backspace to go back
|
|
||||||
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) {
|
|
||||||
if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right) {
|
|
||||||
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_Left:
|
|
||||||
if (backButtonFocused)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (selectedIndex > 0) {
|
|
||||||
selectedIndex--
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Right:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Up:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
// Go to first row, appropriate column
|
|
||||||
var col = selectedIndex % gridColumns
|
|
||||||
selectedIndex = Math.min(col, totalItems - 1)
|
|
||||||
} else if (selectedIndex >= gridColumns) {
|
|
||||||
// Move up one row
|
|
||||||
selectedIndex -= gridColumns
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
// At top row, go to back button
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Down:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else {
|
|
||||||
// Move down one row if possible
|
|
||||||
var newIndex = selectedIndex + gridColumns
|
|
||||||
if (newIndex < totalItems) {
|
|
||||||
selectedIndex = newIndex
|
|
||||||
} else {
|
|
||||||
// If can't go down a full row, go to last item in the column if exists
|
|
||||||
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)
|
|
||||||
// Trigger selection by setting the grid's current index and using signal
|
|
||||||
fileBrowserModal.keyboardFileSelection(selectedIndex)
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: keyboardSelectionTimer
|
|
||||||
|
|
||||||
property int targetIndex: -1
|
|
||||||
|
|
||||||
interval: 1
|
|
||||||
onTriggered: {
|
|
||||||
// Access the currently selected item through model role names
|
|
||||||
// This will work because QML models expose role data
|
|
||||||
executeKeyboardSelection(targetIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: weDiscoveryProcess
|
|
||||||
|
|
||||||
property string wePath: ""
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
fileBrowserModal.weAvailable = true
|
|
||||||
fileBrowserModal.wePath = wePath
|
|
||||||
} else {
|
|
||||||
currentPathIndex++
|
|
||||||
checkNextPath()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: browserIcon
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: browserTitle
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "movie"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: weMode ? Theme.primary : Theme.surfaceText
|
|
||||||
visible: weAvailable && browserType === "wallpaper"
|
|
||||||
onClicked: {
|
|
||||||
weMode = !weMode
|
|
||||||
if (weMode) {
|
|
||||||
navigateTo(wePath)
|
|
||||||
} else {
|
|
||||||
navigateTo(getLastPath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
|
|
||||||
opacity: currentPath !== homeDir ? 1 : 0
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "arrow_back"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: backButtonMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: currentPath !== homeDir
|
|
||||||
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: currentPath !== homeDir
|
|
||||||
onClicked: navigateUp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: fileBrowserModal.currentPath.replace("file://", "")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
width: parent.width - 40 - Theme.spacingS
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
maximumLineCount: 1
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: fileGrid
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 80
|
|
||||||
clip: true
|
|
||||||
cellWidth: weMode ? 255 : 150
|
|
||||||
cellHeight: weMode ? 215 : 130
|
|
||||||
cacheBuffer: 260
|
|
||||||
model: folderModel
|
|
||||||
currentIndex: selectedIndex
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive && currentIndex >= 0)
|
|
||||||
positionViewAtIndex(currentIndex, GridView.Contain)
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: StyledRect {
|
|
||||||
id: delegateRoot
|
|
||||||
|
|
||||||
required property bool fileIsDir
|
|
||||||
required property string filePath
|
|
||||||
required property string fileName
|
|
||||||
required property url fileURL
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: weMode ? 245 : 140
|
|
||||||
height: weMode ? 205 : 120
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
|
||||||
return Theme.surfacePressed
|
|
||||||
|
|
||||||
return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
||||||
}
|
|
||||||
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline
|
|
||||||
border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0
|
|
||||||
// Update file info when this item gets selected via keyboard or initially
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
|
||||||
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for selectedIndex changes to update file info during keyboard navigation
|
|
||||||
Connections {
|
|
||||||
function onSelectedIndexChanged() {
|
|
||||||
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
|
|
||||||
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
target: fileBrowserModal
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: weMode ? 225 : 80
|
|
||||||
height: weMode ? 165 : 60
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
CachingImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
|
|
||||||
property int weExtIndex: 0
|
|
||||||
source: {
|
|
||||||
if (weMode && delegateRoot.fileIsDir) {
|
|
||||||
return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
|
|
||||||
}
|
|
||||||
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
|
|
||||||
}
|
|
||||||
onStatusChanged: {
|
|
||||||
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
|
|
||||||
if (weExtIndex < weExtensions.length - 1) {
|
|
||||||
weExtIndex++
|
|
||||||
source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
|
|
||||||
} else {
|
|
||||||
source = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
visible: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir)
|
|
||||||
maxCacheSize: weMode ? 225 : 80
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "description"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "folder"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
visible: delegateRoot.fileIsDir && !weMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: delegateRoot.fileName || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: 120
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
maximumLineCount: 2
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
// Update selected file info and index first
|
|
||||||
selectedIndex = delegateRoot.index
|
|
||||||
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
|
||||||
if (weMode && delegateRoot.fileIsDir) {
|
|
||||||
var sceneId = delegateRoot.filePath.split("/").pop()
|
|
||||||
fileSelected("we:" + sceneId)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
} else if (delegateRoot.fileIsDir) {
|
|
||||||
navigateTo(delegateRoot.filePath)
|
|
||||||
} else {
|
|
||||||
fileSelected(delegateRoot.filePath)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle keyboard selection
|
|
||||||
Connections {
|
|
||||||
function onKeyboardSelectionRequestedChanged() {
|
|
||||||
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) {
|
|
||||||
fileBrowserModal.keyboardSelectionRequested = false
|
|
||||||
selectedIndex = delegateRoot.index
|
|
||||||
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
|
||||||
if (weMode && delegateRoot.fileIsDir) {
|
|
||||||
var sceneId = delegateRoot.filePath.split("/").pop()
|
|
||||||
fileSelected("we:" + sceneId)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
} else if (delegateRoot.fileIsDir) {
|
|
||||||
navigateTo(delegateRoot.filePath)
|
|
||||||
} else {
|
|
||||||
fileSelected(delegateRoot.filePath)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: fileBrowserModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: saveRow
|
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
height: saveMode ? 40 : 0
|
|
||||||
visible: saveMode
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: fileNameInput
|
|
||||||
|
|
||||||
width: parent.width - saveButton.width - Theme.spacingM
|
|
||||||
height: 40
|
|
||||||
text: defaultFileName
|
|
||||||
placeholderText: "Enter filename..."
|
|
||||||
ignoreLeftRightKeys: false
|
|
||||||
focus: saveMode
|
|
||||||
topPadding: Theme.spacingS
|
|
||||||
bottomPadding: Theme.spacingS
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (saveMode)
|
|
||||||
Qt.callLater(() => {
|
|
||||||
forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onAccepted: {
|
|
||||||
if (text.trim() !== "") {
|
|
||||||
// Remove file:// protocol from currentPath if present for proper construction
|
|
||||||
var basePath = currentPath.replace(/^file:\/\//, '')
|
|
||||||
var fullPath = basePath + "/" + text.trim()
|
|
||||||
// Ensure consistent path format - remove any double slashes and normalize
|
|
||||||
fullPath = fullPath.replace(/\/+/g, '/')
|
|
||||||
handleSaveFile(fullPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
id: saveButton
|
|
||||||
|
|
||||||
width: 80
|
|
||||||
height: 40
|
|
||||||
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Save"
|
|
||||||
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
stateColor: Theme.primary
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
enabled: fileNameInput.text.trim() !== ""
|
|
||||||
onClicked: {
|
|
||||||
if (fileNameInput.text.trim() !== "") {
|
|
||||||
// Remove file:// protocol from currentPath if present for proper construction
|
|
||||||
var basePath = currentPath.replace(/^file:\/\//, '')
|
|
||||||
var fullPath = basePath + "/" + fileNameInput.text.trim()
|
|
||||||
// Ensure consistent path format - remove any double slashes and normalize
|
|
||||||
fullPath = fullPath.replace(/\/+/g, '/')
|
|
||||||
handleSaveFile(fullPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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() : ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite confirmation dialog
|
|
||||||
Item {
|
|
||||||
id: overwriteDialog
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: showOverwriteConfirmation
|
|
||||||
|
|
||||||
Keys.onEscapePressed: {
|
|
||||||
showOverwriteConfirmation = false
|
|
||||||
pendingFilePath = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
|
||||||
showOverwriteConfirmation = false
|
|
||||||
fileSelected(pendingFilePath)
|
|
||||||
pendingFilePath = ""
|
|
||||||
Qt.callLater(() => fileBrowserModal.close())
|
|
||||||
}
|
|
||||||
|
|
||||||
focus: showOverwriteConfirmation
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Theme.shadowStrong
|
|
||||||
opacity: 0.8
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
showOverwriteConfirmation = false
|
|
||||||
pendingFilePath = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 400
|
|
||||||
height: 160
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingL * 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: qsTr("File Already Exists")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: qsTr("A file with this name already exists. Do you want to overwrite it?")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: 80
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: qsTr("Cancel")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cancelArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
showOverwriteConfirmation = false
|
|
||||||
pendingFilePath = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: 90
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: qsTr("Overwrite")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.background
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: overwriteArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
showOverwriteConfirmation = false
|
|
||||||
fileSelected(pendingFilePath)
|
|
||||||
pendingFilePath = ""
|
|
||||||
Qt.callLater(() => fileBrowserModal.close())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +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
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Battery not detected - only AC power settings available"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: !BatteryService.batteryAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: timeoutSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: timeoutSection
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "schedule"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Idle Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: Math.max(0, parent.width - parent.children[0].width - parent.children[1].width - powerCategory.width - Theme.spacingM * 3)
|
|
||||||
height: parent.height
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButtonGroup {
|
|
||||||
id: powerCategory
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: BatteryService.batteryAvailable
|
|
||||||
model: ["AC Power", "Battery"]
|
|
||||||
currentIndex: 0
|
|
||||||
selectionMode: "single"
|
|
||||||
checkEnabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
id: lockDropdown
|
|
||||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
|
||||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
text: "Automatically lock after"
|
|
||||||
options: timeoutOptions
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: powerCategory
|
|
||||||
function onCurrentIndexChanged() {
|
|
||||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
|
|
||||||
const index = lockDropdown.timeoutValues.indexOf(currentTimeout)
|
|
||||||
lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
|
|
||||||
const index = timeoutValues.indexOf(currentTimeout)
|
|
||||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
|
||||||
}
|
|
||||||
|
|
||||||
onValueChanged: value => {
|
|
||||||
const index = timeoutOptions.indexOf(value)
|
|
||||||
if (index >= 0) {
|
|
||||||
const timeout = timeoutValues[index]
|
|
||||||
if (powerCategory.currentIndex === 0) {
|
|
||||||
SessionData.setAcLockTimeout(timeout)
|
|
||||||
} else {
|
|
||||||
SessionData.setBatteryLockTimeout(timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
id: monitorDropdown
|
|
||||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
|
||||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
text: "Turn off monitors after"
|
|
||||||
options: timeoutOptions
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: powerCategory
|
|
||||||
function onCurrentIndexChanged() {
|
|
||||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
|
|
||||||
const index = monitorDropdown.timeoutValues.indexOf(currentTimeout)
|
|
||||||
monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
|
|
||||||
const index = timeoutValues.indexOf(currentTimeout)
|
|
||||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
|
||||||
}
|
|
||||||
|
|
||||||
onValueChanged: value => {
|
|
||||||
const index = timeoutOptions.indexOf(value)
|
|
||||||
if (index >= 0) {
|
|
||||||
const timeout = timeoutValues[index]
|
|
||||||
if (powerCategory.currentIndex === 0) {
|
|
||||||
SessionData.setAcMonitorTimeout(timeout)
|
|
||||||
} else {
|
|
||||||
SessionData.setBatteryMonitorTimeout(timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
id: suspendDropdown
|
|
||||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
|
||||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
text: "Suspend system after"
|
|
||||||
options: timeoutOptions
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: powerCategory
|
|
||||||
function onCurrentIndexChanged() {
|
|
||||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
|
|
||||||
const index = suspendDropdown.timeoutValues.indexOf(currentTimeout)
|
|
||||||
suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
|
|
||||||
const index = timeoutValues.indexOf(currentTimeout)
|
|
||||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
|
||||||
}
|
|
||||||
|
|
||||||
onValueChanged: value => {
|
|
||||||
const index = timeoutOptions.indexOf(value)
|
|
||||||
if (index >= 0) {
|
|
||||||
const timeout = timeoutValues[index]
|
|
||||||
if (powerCategory.currentIndex === 0) {
|
|
||||||
SessionData.setAcSuspendTimeout(timeout)
|
|
||||||
} else {
|
|
||||||
SessionData.setBatterySuspendTimeout(timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
id: hibernateDropdown
|
|
||||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
|
||||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
text: "Hibernate system after"
|
|
||||||
options: timeoutOptions
|
|
||||||
visible: SessionService.hibernateSupported
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: powerCategory
|
|
||||||
function onCurrentIndexChanged() {
|
|
||||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
|
|
||||||
const index = hibernateDropdown.timeoutValues.indexOf(currentTimeout)
|
|
||||||
hibernateDropdown.currentValue = index >= 0 ? hibernateDropdown.timeoutOptions[index] : "Never"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
|
|
||||||
const index = timeoutValues.indexOf(currentTimeout)
|
|
||||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
|
||||||
}
|
|
||||||
|
|
||||||
onValueChanged: value => {
|
|
||||||
const index = timeoutOptions.indexOf(value)
|
|
||||||
if (index >= 0) {
|
|
||||||
const timeout = timeoutValues[index]
|
|
||||||
if (powerCategory.currentIndex === 0) {
|
|
||||||
SessionData.setAcHibernateTimeout(timeout)
|
|
||||||
} else {
|
|
||||||
SessionData.setBatteryHibernateTimeout(timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "Lock before suspend"
|
|
||||||
description: "Automatically lock the screen when the system prepares to suspend"
|
|
||||||
checked: SessionData.lockBeforeSuspend
|
|
||||||
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Idle monitoring not supported - requires newer Quickshell version"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.error
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: !IdleService.idleMonitorAvailable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
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": "Personalization",
|
|
||||||
"icon": "person"
|
|
||||||
}, {
|
|
||||||
"text": "Time & Date",
|
|
||||||
"icon": "schedule"
|
|
||||||
}, {
|
|
||||||
"text": "Weather",
|
|
||||||
"icon": "cloud"
|
|
||||||
}, {
|
|
||||||
"text": "Top Bar",
|
|
||||||
"icon": "toolbar"
|
|
||||||
}, {
|
|
||||||
"text": "Widgets",
|
|
||||||
"icon": "widgets"
|
|
||||||
}, {
|
|
||||||
"text": "Dock",
|
|
||||||
"icon": "dock_to_bottom"
|
|
||||||
}, {
|
|
||||||
"text": "Displays",
|
|
||||||
"icon": "monitor"
|
|
||||||
}, {
|
|
||||||
"text": "Launcher",
|
|
||||||
"icon": "apps"
|
|
||||||
}, {
|
|
||||||
"text": "Theme & Colors",
|
|
||||||
"icon": "palette"
|
|
||||||
}, {
|
|
||||||
"text": "Power",
|
|
||||||
"icon": "power_settings_new"
|
|
||||||
}, {
|
|
||||||
"text": "About",
|
|
||||||
"icon": "info"
|
|
||||||
}]
|
|
||||||
|
|
||||||
width: 270
|
|
||||||
height: parent.height
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.topMargin: Theme.spacingM + 2
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
ProfileSection {
|
|
||||||
parentModal: sidebarContainer.parentModal
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Theme.spacingL
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: sidebarRepeater
|
|
||||||
|
|
||||||
model: sidebarContainer.sidebarItems
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool isActive: sidebarContainer.currentIndex === index
|
|
||||||
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 44
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: isActive ? Theme.primaryContainer : 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.surfaceText : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.text || ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: parent.parent.isActive ? Theme.surfaceText : 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,221 +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 var parentModal: null
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
if (parentModal)
|
|
||||||
parentModal.hide()
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
appLauncher.selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
appLauncher.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
|
|
||||||
searchField.forceActiveFocus()
|
|
||||||
searchField.insertText(event.text)
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppLauncher {
|
|
||||||
id: appLauncher
|
|
||||||
|
|
||||||
viewMode: SettingsData.spotlightModalViewMode
|
|
||||||
gridColumns: 4
|
|
||||||
onAppLaunched: () => {
|
|
||||||
if (parentModal)
|
|
||||||
parentModal.hide()
|
|
||||||
}
|
|
||||||
onViewModeSelected: mode => {
|
|
||||||
SettingsData.setSpotlightModalViewMode(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: categorySelector.height + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0
|
|
||||||
|
|
||||||
CategorySelector {
|
|
||||||
id: categorySelector
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
categories: appLauncher.categories
|
|
||||||
selectedCategory: appLauncher.selectedCategory
|
|
||||||
compact: false
|
|
||||||
onCategorySelected: category => {
|
|
||||||
appLauncher.setCategory(category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
leftPadding: Theme.spacingS
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
|
|
||||||
width: parent.width - 80 - Theme.spacingL
|
|
||||||
height: 56
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
backgroundColor: Theme.surfaceContainerHigh
|
|
||||||
normalBorderColor: Theme.outlineMedium
|
|
||||||
focusedBorderColor: Theme.primary
|
|
||||||
leftIconName: "search"
|
|
||||||
leftIconSize: Theme.iconSize
|
|
||||||
leftIconColor: Theme.surfaceVariantText
|
|
||||||
leftIconFocusedColor: Theme.primary
|
|
||||||
showClearButton: true
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
enabled: parentModal ? parentModal.spotlightOpen : true
|
|
||||||
placeholderText: ""
|
|
||||||
ignoreLeftRightKeys: true
|
|
||||||
keyForwardTargets: [spotlightKeyHandler]
|
|
||||||
text: appLauncher.searchQuery
|
|
||||||
onTextEdited: () => {
|
|
||||||
appLauncher.searchQuery = text
|
|
||||||
}
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
if (parentModal)
|
|
||||||
parentModal.hide()
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
|
||||||
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
else if (appLauncher.model.count > 0)
|
|
||||||
appLauncher.launchApp(appLauncher.model.get(0))
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: appLauncher.model.count > 0
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "view_list"
|
|
||||||
size: 18
|
|
||||||
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: listViewArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
appLauncher.setViewMode("list")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "grid_view"
|
|
||||||
size: 18
|
|
||||||
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gridViewArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
appLauncher.setViewMode("grid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotlightResults {
|
|
||||||
appLauncher: spotlightKeyHandler.appLauncher
|
|
||||||
contextMenu: contextMenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotlightContextMenu {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
appLauncher: spotlightKeyHandler.appLauncher
|
|
||||||
parentHandler: spotlightKeyHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: contextMenu.visible
|
|
||||||
z: 999
|
|
||||||
onClicked: () => {
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
|
|
||||||
// Prevent closing when clicking on the menu itself
|
|
||||||
x: contextMenu.x
|
|
||||||
y: contextMenu.y
|
|
||||||
width: contextMenu.width
|
|
||||||
height: contextMenu.height
|
|
||||||
onClicked: () => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
property var currentApp: null
|
|
||||||
property bool menuVisible: false
|
|
||||||
property var appLauncher: null
|
|
||||||
property var parentHandler: null
|
|
||||||
|
|
||||||
function show(x, y, app) {
|
|
||||||
currentApp = app
|
|
||||||
const menuWidth = 180
|
|
||||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
let finalX = x + 8
|
|
||||||
let finalY = y + 8
|
|
||||||
if (parentHandler) {
|
|
||||||
if (finalX + menuWidth > parentHandler.width)
|
|
||||||
finalX = x - menuWidth - 8
|
|
||||||
|
|
||||||
if (finalY + menuHeight > parentHandler.height)
|
|
||||||
finalY = y - menuHeight - 8
|
|
||||||
|
|
||||||
finalX = Math.max(8, Math.min(finalX, parentHandler.width - menuWidth - 8))
|
|
||||||
finalY = Math.max(8, Math.min(finalY, parentHandler.height - menuHeight - 8))
|
|
||||||
}
|
|
||||||
contextMenu.x = finalX
|
|
||||||
contextMenu.y = finalY
|
|
||||||
contextMenu.visible = true
|
|
||||||
contextMenu.menuVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
contextMenu.menuVisible = false
|
|
||||||
Qt.callLater(() => {
|
|
||||||
contextMenu.visible = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 180
|
|
||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
opacity: menuVisible ? 1 : 0
|
|
||||||
scale: menuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "push_pin"
|
|
||||||
|
|
||||||
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
|
||||||
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "Pin to Dock"
|
|
||||||
|
|
||||||
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
|
||||||
return SessionData.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: pinMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
|
||||||
return
|
|
||||||
|
|
||||||
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
|
||||||
if (SessionData.isPinnedApp(appId))
|
|
||||||
SessionData.removePinnedApp(appId)
|
|
||||||
else
|
|
||||||
SessionData.addPinnedApp(appId)
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "launch"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "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.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +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
|
|
||||||
|
|
||||||
property bool spotlightOpen: false
|
|
||||||
property Component spotlightContent
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
spotlightOpen = true
|
|
||||||
open()
|
|
||||||
if (contentLoader.item && contentLoader.item.appLauncher) {
|
|
||||||
contentLoader.item.appLauncher.searchQuery = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item && contentLoader.item.searchField) {
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
spotlightOpen = false
|
|
||||||
close()
|
|
||||||
if (contentLoader.item && contentLoader.item.appLauncher) {
|
|
||||||
contentLoader.item.appLauncher.searchQuery = ""
|
|
||||||
contentLoader.item.appLauncher.selectedIndex = 0
|
|
||||||
contentLoader.item.appLauncher.setCategory("All")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (spotlightOpen) {
|
|
||||||
hide()
|
|
||||||
} else {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: spotlightOpen
|
|
||||||
width: 550
|
|
||||||
height: 600
|
|
||||||
backgroundColor: Theme.popupBackground()
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
borderColor: Theme.outlineMedium
|
|
||||||
borderWidth: 1
|
|
||||||
enableShadow: true
|
|
||||||
onVisibleChanged: () => {
|
|
||||||
if (visible && !spotlightOpen) {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
if (visible && contentLoader.item) {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item.searchField) {
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return hide()
|
|
||||||
}
|
|
||||||
content: spotlightContent
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
|
|
||||||
spotlightOpen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: ModalManager
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
spotlightModal.show()
|
|
||||||
return "SPOTLIGHT_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
spotlightModal.hide()
|
|
||||||
return "SPOTLIGHT_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
spotlightModal.toggle()
|
|
||||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "spotlight"
|
|
||||||
}
|
|
||||||
|
|
||||||
spotlightContent: Component {
|
|
||||||
SpotlightContent {
|
|
||||||
parentModal: spotlightModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,318 +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
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - y
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: resultsList
|
|
||||||
|
|
||||||
property int itemHeight: 60
|
|
||||||
property int iconSize: 40
|
|
||||||
property bool showDescription: true
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
const itemY = index * (itemHeight + itemSpacing)
|
|
||||||
const itemBottom = itemY + itemHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
visible: appLauncher && appLauncher.viewMode === "list"
|
|
||||||
model: appLauncher ? appLauncher.model : null
|
|
||||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
|
||||||
clip: true
|
|
||||||
spacing: itemSpacing
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
onItemClicked: (index, modelData) => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
|
||||||
if (contextMenu)
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: () => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: ListView.view.width
|
|
||||||
height: resultsList.itemHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: resultsList.iconSize
|
|
||||||
height: resultsList.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: listIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Quickshell.iconPath(model.icon, true)
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !listIconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
|
||||||
font.pixelSize: resultsList.iconSize * 0.4
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - resultsList.iconSize - Theme.spacingL
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.comment || "Application"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
visible: resultsList.showDescription && model.comment && model.comment.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: listMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
z: 10
|
|
||||||
onEntered: () => {
|
|
||||||
if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive)
|
|
||||||
resultsList.currentIndex = index
|
|
||||||
}
|
|
||||||
onPositionChanged: () => {
|
|
||||||
resultsList.keyboardNavigationReset()
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
resultsList.itemClicked(index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
|
|
||||||
resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: resultsGrid
|
|
||||||
|
|
||||||
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
|
||||||
property int columns: 4
|
|
||||||
property bool adaptiveColumns: false
|
|
||||||
property int minCellWidth: 120
|
|
||||||
property int maxCellWidth: 160
|
|
||||||
property int cellPadding: 8
|
|
||||||
property real iconSizeRatio: 0.55
|
|
||||||
property int maxIconSize: 48
|
|
||||||
property int minIconSize: 32
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
|
||||||
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
|
||||||
property int baseCellHeight: baseCellWidth + 20
|
|
||||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
|
||||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
const itemY = Math.floor(index / actualColumns) * cellHeight
|
|
||||||
const itemBottom = itemY + cellHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
visible: appLauncher && appLauncher.viewMode === "grid"
|
|
||||||
model: appLauncher ? appLauncher.model : null
|
|
||||||
clip: true
|
|
||||||
cellWidth: baseCellWidth
|
|
||||||
cellHeight: baseCellHeight
|
|
||||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
|
||||||
rightMargin: leftMargin
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
onItemClicked: (index, modelData) => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
|
||||||
if (contextMenu)
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: () => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: resultsGrid.cellWidth - resultsGrid.cellPadding
|
|
||||||
height: resultsGrid.cellHeight - resultsGrid.cellPadding
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio))
|
|
||||||
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: gridIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Quickshell.iconPath(model.icon, true)
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !gridIconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
|
||||||
font.pixelSize: Math.min(28, parent.width * 0.5)
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
width: resultsGrid.cellWidth - 12
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
maximumLineCount: 2
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gridMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
z: 10
|
|
||||||
onEntered: () => {
|
|
||||||
if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive)
|
|
||||||
resultsGrid.currentIndex = index
|
|
||||||
}
|
|
||||||
onPositionChanged: () => {
|
|
||||||
resultsGrid.keyboardNavigationReset()
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
resultsGrid.itemClicked(index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
|
|
||||||
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string wifiPasswordSSID: ""
|
|
||||||
property string wifiPasswordInput: ""
|
|
||||||
|
|
||||||
function show(ssid) {
|
|
||||||
wifiPasswordSSID = ssid
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
open()
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item && contentLoader.item.passwordInput)
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
width: 420
|
|
||||||
height: 230
|
|
||||||
onShouldBeVisibleChanged: () => {
|
|
||||||
if (!shouldBeVisible)
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
|
||||||
onOpened: {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item && contentLoader.item.passwordInput)
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
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 passwordInput: passwordInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
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: "Connect to Wi-Fi"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `Enter password for "${wifiPasswordSSID}"`
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: passwordInput.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
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: ""
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
focus: true
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiPasswordInput = text
|
|
||||||
}
|
|
||||||
onAccepted: () => {
|
|
||||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
passwordInput.text = ""
|
|
||||||
}
|
|
||||||
Component.onCompleted: () => {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: focusDelayTimer
|
|
||||||
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
onTriggered: () => {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: showPasswordCheckbox
|
|
||||||
|
|
||||||
property bool checked: false
|
|
||||||
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
radius: 4
|
|
||||||
color: checked ? Theme.primary : "transparent"
|
|
||||||
border.color: checked ? Theme.primary : Theme.outlineButton
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "check"
|
|
||||||
size: 12
|
|
||||||
color: Theme.background
|
|
||||||
visible: parent.checked
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show password"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: cancelText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Cancel"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cancelArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
|
||||||
enabled: passwordInput.text.length > 0
|
|
||||||
opacity: enabled ? 1 : 0.5
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: connectText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Connect"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.background
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: connectArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: parent.enabled
|
|
||||||
onClicked: () => {
|
|
||||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
passwordInput.text = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string searchQuery: ""
|
|
||||||
property string selectedCategory: "All"
|
|
||||||
property string viewMode: "list" // "list" or "grid"
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int maxResults: 50
|
|
||||||
property int gridColumns: 4
|
|
||||||
property bool debounceSearch: true
|
|
||||||
property int debounceInterval: 50
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property bool suppressUpdatesWhileLaunching: false
|
|
||||||
readonly property var categories: {
|
|
||||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
|
||||||
const result = ["All"]
|
|
||||||
return result.concat(allCategories.filter(cat => cat !== "All"))
|
|
||||||
}
|
|
||||||
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
|
|
||||||
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
|
||||||
property alias model: filteredModel
|
|
||||||
property var _watchApplications: AppSearchService.applications
|
|
||||||
|
|
||||||
signal appLaunched(var app)
|
|
||||||
signal categorySelected(string category)
|
|
||||||
signal viewModeSelected(string mode)
|
|
||||||
|
|
||||||
function updateFilteredModel() {
|
|
||||||
if (suppressUpdatesWhileLaunching) {
|
|
||||||
suppressUpdatesWhileLaunching = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
filteredModel.clear()
|
|
||||||
selectedIndex = 0
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
|
|
||||||
let apps = []
|
|
||||||
if (searchQuery.length === 0) {
|
|
||||||
apps = selectedCategory === "All" ? AppSearchService.getAppsInCategory("All") : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
|
||||||
} else {
|
|
||||||
if (selectedCategory === "All") {
|
|
||||||
apps = AppSearchService.searchApplications(searchQuery)
|
|
||||||
} else {
|
|
||||||
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
|
||||||
if (categoryApps.length > 0) {
|
|
||||||
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
|
||||||
const categoryNames = new Set(categoryApps.map(app => app.name))
|
|
||||||
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
|
||||||
} else {
|
|
||||||
apps = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchQuery.length === 0) {
|
|
||||||
apps = apps.sort((a, b) => {
|
|
||||||
const aId = a.id || a.execString || a.exec || ""
|
|
||||||
const bId = b.id || b.execString || b.exec || ""
|
|
||||||
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
|
||||||
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
|
||||||
if (aUsage !== bUsage) {
|
|
||||||
return bUsage - aUsage
|
|
||||||
}
|
|
||||||
return (a.name || "").localeCompare(b.name || "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
apps.forEach(app => {
|
|
||||||
if (app) {
|
|
||||||
filteredModel.append({
|
|
||||||
"name": app.name || "",
|
|
||||||
"exec": app.execString || "",
|
|
||||||
"icon": app.icon || "application-x-executable",
|
|
||||||
"comment": app.comment || "",
|
|
||||||
"categories": app.categories || [],
|
|
||||||
"desktopEntry": app
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (filteredModel.count === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (filteredModel.count === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNextInRow() {
|
|
||||||
if (filteredModel.count === 0 || viewMode !== "grid") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPreviousInRow() {
|
|
||||||
if (filteredModel.count === 0 || viewMode !== "grid") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchSelected() {
|
|
||||||
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const selectedApp = filteredModel.get(selectedIndex)
|
|
||||||
launchApp(selectedApp)
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchApp(appData) {
|
|
||||||
if (!appData) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
suppressUpdatesWhileLaunching = true
|
|
||||||
SessionService.launchDesktopEntry(appData.desktopEntry)
|
|
||||||
appLaunched(appData)
|
|
||||||
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCategory(category) {
|
|
||||||
selectedCategory = category
|
|
||||||
categorySelected(category)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setViewMode(mode) {
|
|
||||||
viewMode = mode
|
|
||||||
viewModeSelected(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchQueryChanged: {
|
|
||||||
if (debounceSearch) {
|
|
||||||
searchDebounceTimer.restart()
|
|
||||||
} else {
|
|
||||||
updateFilteredModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onSelectedCategoryChanged: updateFilteredModel()
|
|
||||||
onAppUsageRankingChanged: updateFilteredModel()
|
|
||||||
on_WatchApplicationsChanged: updateFilteredModel()
|
|
||||||
Component.onCompleted: {
|
|
||||||
updateFilteredModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: filteredModel
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: searchDebounceTimer
|
|
||||||
|
|
||||||
interval: root.debounceInterval
|
|
||||||
repeat: false
|
|
||||||
onTriggered: updateFilteredModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ControlCenter.Details
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string expandedSection: ""
|
|
||||||
property var expandedWidgetData: null
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
height: 250
|
|
||||||
y: Theme.spacingS
|
|
||||||
active: parent.height > 0
|
|
||||||
property string sectionKey: root.expandedSection
|
|
||||||
sourceComponent: {
|
|
||||||
switch (root.expandedSection) {
|
|
||||||
case "network":
|
|
||||||
case "wifi": return networkDetailComponent
|
|
||||||
case "bluetooth": return bluetoothDetailComponent
|
|
||||||
case "audioOutput": return audioOutputDetailComponent
|
|
||||||
case "audioInput": return audioInputDetailComponent
|
|
||||||
case "battery": return batteryDetailComponent
|
|
||||||
default:
|
|
||||||
if (root.expandedSection.startsWith("diskUsage_")) {
|
|
||||||
return diskUsageDetailComponent
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onSectionKeyChanged: {
|
|
||||||
active = false
|
|
||||||
active = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: networkDetailComponent
|
|
||||||
NetworkDetail {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: bluetoothDetailComponent
|
|
||||||
BluetoothDetail {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: audioOutputDetailComponent
|
|
||||||
AudioOutputDetail {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: audioInputDetailComponent
|
|
||||||
AudioInputDetail {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: batteryDetailComponent
|
|
||||||
BatteryDetail {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: diskUsageDetailComponent
|
|
||||||
DiskUsageDetail {
|
|
||||||
currentMountPath: root.expandedWidgetData?.mountPath || "/"
|
|
||||||
instanceId: root.expandedWidgetData?.instanceId || ""
|
|
||||||
|
|
||||||
|
|
||||||
onMountPathChanged: (newMountPath) => {
|
|
||||||
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
|
|
||||||
const widgets = SettingsData.controlCenterWidgets || []
|
|
||||||
const newWidgets = widgets.map(w => {
|
|
||||||
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
|
|
||||||
const updatedWidget = Object.assign({}, w)
|
|
||||||
updatedWidget.mountPath = newMountPath
|
|
||||||
return updatedWidget
|
|
||||||
}
|
|
||||||
return w
|
|
||||||
})
|
|
||||||
SettingsData.setControlCenterWidgets(newWidgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool expanded: false
|
|
||||||
|
|
||||||
signal powerActionRequested(string action, string title, string message)
|
|
||||||
|
|
||||||
implicitHeight: expanded ? 60 : 0
|
|
||||||
height: implicitHeight
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: root.expanded ? 1 : 0
|
|
||||||
opacity: root.expanded ? 1 : 0
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: SessionService.hibernateSupported ? Theme.spacingS : Theme.spacingL
|
|
||||||
visible: root.expanded
|
|
||||||
|
|
||||||
PowerButton {
|
|
||||||
width: SessionService.hibernateSupported ? 85 : 100
|
|
||||||
iconName: "logout"
|
|
||||||
text: "Logout"
|
|
||||||
onPressed: root.powerActionRequested("logout", "Logout", "Are you sure you want to logout?")
|
|
||||||
}
|
|
||||||
|
|
||||||
PowerButton {
|
|
||||||
width: SessionService.hibernateSupported ? 85 : 100
|
|
||||||
iconName: "restart_alt"
|
|
||||||
text: "Restart"
|
|
||||||
onPressed: root.powerActionRequested("reboot", "Restart", "Are you sure you want to restart?")
|
|
||||||
}
|
|
||||||
|
|
||||||
PowerButton {
|
|
||||||
width: SessionService.hibernateSupported ? 85 : 100
|
|
||||||
iconName: "bedtime"
|
|
||||||
text: "Suspend"
|
|
||||||
onPressed: root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend?")
|
|
||||||
}
|
|
||||||
|
|
||||||
PowerButton {
|
|
||||||
width: SessionService.hibernateSupported ? 85 : 100
|
|
||||||
iconName: "ac_unit"
|
|
||||||
text: "Hibernate"
|
|
||||||
visible: SessionService.hibernateSupported
|
|
||||||
onPressed: root.powerActionRequested("hibernate", "Hibernate", "Are you sure you want to hibernate?")
|
|
||||||
}
|
|
||||||
|
|
||||||
PowerButton {
|
|
||||||
width: SessionService.hibernateSupported ? 85 : 100
|
|
||||||
iconName: "power_settings_new"
|
|
||||||
text: "Shutdown"
|
|
||||||
onPressed: root.powerActionRequested("poweroff", "Shutdown", "Are you sure you want to shutdown?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import "../utils/widgets.js" as WidgetUtils
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property var baseWidgetDefinitions: [
|
|
||||||
{
|
|
||||||
"id": "nightMode",
|
|
||||||
"text": "Night Mode",
|
|
||||||
"description": "Blue light filter",
|
|
||||||
"icon": "nightlight",
|
|
||||||
"type": "toggle",
|
|
||||||
"enabled": DisplayService.automationAvailable,
|
|
||||||
"warning": !DisplayService.automationAvailable ? "Requires night mode support" : undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "darkMode",
|
|
||||||
"text": "Dark Mode",
|
|
||||||
"description": "System theme toggle",
|
|
||||||
"icon": "contrast",
|
|
||||||
"type": "toggle",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "doNotDisturb",
|
|
||||||
"text": "Do Not Disturb",
|
|
||||||
"description": "Block notifications",
|
|
||||||
"icon": "do_not_disturb_on",
|
|
||||||
"type": "toggle",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "idleInhibitor",
|
|
||||||
"text": "Keep Awake",
|
|
||||||
"description": "Prevent screen timeout",
|
|
||||||
"icon": "motion_sensor_active",
|
|
||||||
"type": "toggle",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "wifi",
|
|
||||||
"text": "Network",
|
|
||||||
"description": "Wi-Fi and Ethernet connection",
|
|
||||||
"icon": "wifi",
|
|
||||||
"type": "connection",
|
|
||||||
"enabled": NetworkService.wifiAvailable,
|
|
||||||
"warning": !NetworkService.wifiAvailable ? "Wi-Fi not available" : undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bluetooth",
|
|
||||||
"text": "Bluetooth",
|
|
||||||
"description": "Device connections",
|
|
||||||
"icon": "bluetooth",
|
|
||||||
"type": "connection",
|
|
||||||
"enabled": BluetoothService.available,
|
|
||||||
"warning": !BluetoothService.available ? "Bluetooth not available" : undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "audioOutput",
|
|
||||||
"text": "Audio Output",
|
|
||||||
"description": "Speaker settings",
|
|
||||||
"icon": "volume_up",
|
|
||||||
"type": "connection",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "audioInput",
|
|
||||||
"text": "Audio Input",
|
|
||||||
"description": "Microphone settings",
|
|
||||||
"icon": "mic",
|
|
||||||
"type": "connection",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "volumeSlider",
|
|
||||||
"text": "Volume Slider",
|
|
||||||
"description": "Audio volume control",
|
|
||||||
"icon": "volume_up",
|
|
||||||
"type": "slider",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "brightnessSlider",
|
|
||||||
"text": "Brightness Slider",
|
|
||||||
"description": "Display brightness control",
|
|
||||||
"icon": "brightness_6",
|
|
||||||
"type": "slider",
|
|
||||||
"enabled": DisplayService.brightnessAvailable,
|
|
||||||
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "inputVolumeSlider",
|
|
||||||
"text": "Input Volume Slider",
|
|
||||||
"description": "Microphone volume control",
|
|
||||||
"icon": "mic",
|
|
||||||
"type": "slider",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "battery",
|
|
||||||
"text": "Battery",
|
|
||||||
"description": "Battery and power management",
|
|
||||||
"icon": "battery_std",
|
|
||||||
"type": "action",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "diskUsage",
|
|
||||||
"text": "Disk Usage",
|
|
||||||
"description": "Filesystem usage monitoring",
|
|
||||||
"icon": "storage",
|
|
||||||
"type": "action",
|
|
||||||
"enabled": DgopService.dgopAvailable,
|
|
||||||
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined,
|
|
||||||
"allowMultiple": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
function getWidgetForId(widgetId) {
|
|
||||||
return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWidget(widgetId) {
|
|
||||||
WidgetUtils.addWidget(widgetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWidget(index) {
|
|
||||||
WidgetUtils.removeWidget(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleWidgetSize(index) {
|
|
||||||
WidgetUtils.toggleWidgetSize(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveWidget(fromIndex, toIndex) {
|
|
||||||
WidgetUtils.moveWidget(fromIndex, toIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetToDefault() {
|
|
||||||
WidgetUtils.resetToDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAll() {
|
|
||||||
WidgetUtils.clearAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Widgets
|
|
||||||
|
|
||||||
CompoundPill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var defaultSource: AudioService.source
|
|
||||||
|
|
||||||
iconName: {
|
|
||||||
if (!defaultSource) return "mic_off"
|
|
||||||
|
|
||||||
let volume = defaultSource.audio.volume
|
|
||||||
let muted = defaultSource.audio.muted
|
|
||||||
|
|
||||||
if (muted || volume === 0.0) return "mic_off"
|
|
||||||
return "mic"
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive: defaultSource && !defaultSource.audio.muted
|
|
||||||
|
|
||||||
primaryText: {
|
|
||||||
if (!defaultSource) {
|
|
||||||
return "No input device"
|
|
||||||
}
|
|
||||||
return defaultSource.description || "Audio Input"
|
|
||||||
}
|
|
||||||
|
|
||||||
secondaryText: {
|
|
||||||
if (!defaultSource) {
|
|
||||||
return "Select device"
|
|
||||||
}
|
|
||||||
if (defaultSource.audio.muted) {
|
|
||||||
return "Muted"
|
|
||||||
}
|
|
||||||
return Math.round(defaultSource.audio.volume * 100) + "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggled: {
|
|
||||||
if (defaultSource && defaultSource.audio) {
|
|
||||||
defaultSource.audio.muted = !defaultSource.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onWheelEvent: function (wheelEvent) {
|
|
||||||
if (!defaultSource || !defaultSource.audio) return
|
|
||||||
let delta = wheelEvent.angleDelta.y
|
|
||||||
let currentVolume = defaultSource.audio.volume * 100
|
|
||||||
let newVolume
|
|
||||||
if (delta > 0)
|
|
||||||
newVolume = Math.min(100, currentVolume + 5)
|
|
||||||
else
|
|
||||||
newVolume = Math.max(0, currentVolume - 5)
|
|
||||||
defaultSource.audio.muted = false
|
|
||||||
defaultSource.audio.volume = newVolume / 100
|
|
||||||
wheelEvent.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Widgets
|
|
||||||
|
|
||||||
CompoundPill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var defaultSink: AudioService.sink
|
|
||||||
|
|
||||||
iconName: {
|
|
||||||
if (!defaultSink) return "volume_off"
|
|
||||||
|
|
||||||
let volume = defaultSink.audio.volume
|
|
||||||
let muted = defaultSink.audio.muted
|
|
||||||
|
|
||||||
if (muted || volume === 0.0) return "volume_off"
|
|
||||||
if (volume <= 0.33) return "volume_down"
|
|
||||||
if (volume <= 0.66) return "volume_up"
|
|
||||||
return "volume_up"
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive: defaultSink && !defaultSink.audio.muted
|
|
||||||
|
|
||||||
primaryText: {
|
|
||||||
if (!defaultSink) {
|
|
||||||
return "No output device"
|
|
||||||
}
|
|
||||||
return defaultSink.description || "Audio Output"
|
|
||||||
}
|
|
||||||
|
|
||||||
secondaryText: {
|
|
||||||
if (!defaultSink) {
|
|
||||||
return "Select device"
|
|
||||||
}
|
|
||||||
if (defaultSink.audio.muted) {
|
|
||||||
return "Muted"
|
|
||||||
}
|
|
||||||
return Math.round(defaultSink.audio.volume * 100) + "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggled: {
|
|
||||||
if (defaultSink && defaultSink.audio) {
|
|
||||||
defaultSink.audio.muted = !defaultSink.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onWheelEvent: function (wheelEvent) {
|
|
||||||
if (!defaultSink || !defaultSink.audio) return
|
|
||||||
let delta = wheelEvent.angleDelta.y
|
|
||||||
let currentVolume = defaultSink.audio.volume * 100
|
|
||||||
let newVolume
|
|
||||||
if (delta > 0)
|
|
||||||
newVolume = Math.min(100, currentVolume + 5)
|
|
||||||
else
|
|
||||||
newVolume = Math.max(0, currentVolume - 5)
|
|
||||||
defaultSink.audio.muted = false
|
|
||||||
defaultSink.audio.volume = newVolume / 100
|
|
||||||
AudioService.volumeChanged()
|
|
||||||
wheelEvent.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Widgets
|
|
||||||
|
|
||||||
CompoundPill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var primaryDevice: {
|
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
|
||||||
for (let device of devices) {
|
|
||||||
if (device && device.connected) {
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
iconName: {
|
|
||||||
if (!BluetoothService.available) {
|
|
||||||
return "bluetooth_disabled"
|
|
||||||
}
|
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
|
||||||
return "bluetooth_disabled"
|
|
||||||
}
|
|
||||||
return "bluetooth"
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
|
|
||||||
showExpandArea: BluetoothService.available
|
|
||||||
|
|
||||||
primaryText: {
|
|
||||||
if (!BluetoothService.available) {
|
|
||||||
return "Bluetooth"
|
|
||||||
}
|
|
||||||
if (!BluetoothService.adapter) {
|
|
||||||
return "No adapter"
|
|
||||||
}
|
|
||||||
if (!BluetoothService.adapter.enabled) {
|
|
||||||
return "Disabled"
|
|
||||||
}
|
|
||||||
return "Enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
secondaryText: {
|
|
||||||
if (!BluetoothService.available) {
|
|
||||||
return "No adapters"
|
|
||||||
}
|
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
|
||||||
return "Off"
|
|
||||||
}
|
|
||||||
if (primaryDevice) {
|
|
||||||
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
|
|
||||||
}
|
|
||||||
return "No devices"
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggled: {
|
|
||||||
if (BluetoothService.available && BluetoothService.adapter) {
|
|
||||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
height: 40
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
height: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
|
||||||
color: iconArea.containsMouse
|
|
||||||
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
: "transparent"
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: iconArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: function(event) {
|
|
||||||
if (DisplayService.devices.length > 1) {
|
|
||||||
if (deviceMenu.visible) {
|
|
||||||
deviceMenu.close()
|
|
||||||
} else {
|
|
||||||
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
|
||||||
|
|
||||||
let brightness = DisplayService.brightnessLevel
|
|
||||||
if (brightness <= 33) return "brightness_low"
|
|
||||||
if (brightness <= 66) return "brightness_medium"
|
|
||||||
return "brightness_high"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
|
||||||
enabled: DisplayService.brightnessAvailable
|
|
||||||
minimum: 1
|
|
||||||
maximum: 100
|
|
||||||
value: {
|
|
||||||
let level = DisplayService.brightnessLevel
|
|
||||||
if (level > 100) {
|
|
||||||
let deviceInfo = DisplayService.getCurrentDeviceInfo()
|
|
||||||
if (deviceInfo && deviceInfo.max > 0) {
|
|
||||||
return Math.round((level / deviceInfo.max) * 100)
|
|
||||||
}
|
|
||||||
return 50
|
|
||||||
}
|
|
||||||
return level
|
|
||||||
}
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
DisplayService.setBrightness(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
|
||||||
trackColor: Theme.surfaceContainerHigh
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: deviceMenu
|
|
||||||
width: 200
|
|
||||||
closePolicy: Popup.CloseOnEscape
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 0
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
}
|
|
||||||
|
|
||||||
Instantiator {
|
|
||||||
model: DisplayService.devices
|
|
||||||
delegate: MenuItem {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
property string deviceName: modelData.name || ""
|
|
||||||
property string deviceClass: modelData.class || ""
|
|
||||||
|
|
||||||
text: deviceName
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
indicator: Rectangle {
|
|
||||||
visible: DisplayService.currentDevice === parent.deviceName
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
width: 4
|
|
||||||
height: parent.height - Theme.spacingS * 2
|
|
||||||
radius: 2
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: StyledText {
|
|
||||||
text: parent.text
|
|
||||||
font: parent.font
|
|
||||||
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
|
|
||||||
leftPadding: Theme.spacingL
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
|
||||||
radius: Theme.cornerRadius / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
DisplayService.setCurrentDevice(deviceName, true)
|
|
||||||
deviceMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
|
|
||||||
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Widgets
|
|
||||||
|
|
||||||
CompoundPill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
isActive: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return NetworkService.wifiEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
iconName: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return "sync"
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "settings_ethernet"
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi") {
|
|
||||||
return NetworkService.wifiSignalIcon
|
|
||||||
}
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
return "wifi_off"
|
|
||||||
}
|
|
||||||
return "wifi_off"
|
|
||||||
}
|
|
||||||
|
|
||||||
primaryText: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "Ethernet"
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
|
|
||||||
return NetworkService.currentWifiSSID
|
|
||||||
}
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
return "Not connected"
|
|
||||||
}
|
|
||||||
return "WiFi off"
|
|
||||||
}
|
|
||||||
|
|
||||||
secondaryText: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return "Please wait..."
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "Connected"
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi") {
|
|
||||||
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
|
|
||||||
}
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
return "Select network"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggled: {
|
|
||||||
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
|
|
||||||
NetworkService.toggleWifiRadio()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +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
|
|
||||||
|
|
||||||
property bool dashVisible: false
|
|
||||||
property string triggerSection: "center"
|
|
||||||
property var triggerScreen: null
|
|
||||||
property int currentTabIndex: 0
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
if (section === "center") {
|
|
||||||
const screenWidth = screen ? screen.width : Screen.width
|
|
||||||
triggerX = (screenWidth - popupWidth) / 2
|
|
||||||
triggerWidth = popupWidth
|
|
||||||
} else {
|
|
||||||
triggerX = x
|
|
||||||
triggerWidth = width
|
|
||||||
}
|
|
||||||
triggerY = y
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
}
|
|
||||||
|
|
||||||
popupWidth: 700
|
|
||||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
|
||||||
triggerX: Screen.width - 620 - Theme.spacingL
|
|
||||||
triggerY: Math.max(26 + SettingsData.topBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.topBarInnerPadding)) + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance
|
|
||||||
triggerWidth: 80
|
|
||||||
positioning: "center"
|
|
||||||
shouldBeVisible: dashVisible
|
|
||||||
visible: shouldBeVisible
|
|
||||||
|
|
||||||
|
|
||||||
onDashVisibleChanged: {
|
|
||||||
if (dashVisible) {
|
|
||||||
open()
|
|
||||||
} else {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBackgroundClicked: {
|
|
||||||
dashVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Rectangle {
|
|
||||||
id: mainContainer
|
|
||||||
|
|
||||||
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: function(event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
root.dashVisible = false
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
Qt.callLater(function() {
|
|
||||||
mainContainer.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
model: {
|
|
||||||
let tabs = [
|
|
||||||
{ icon: "dashboard", text: "Overview" },
|
|
||||||
{ icon: "music_note", text: "Media" }
|
|
||||||
]
|
|
||||||
|
|
||||||
if (SettingsData.weatherEnabled) {
|
|
||||||
tabs.push({ icon: "wb_sunny", text: "Weather" })
|
|
||||||
}
|
|
||||||
|
|
||||||
tabs.push({ icon: "settings", text: "Settings", isAction: true })
|
|
||||||
return tabs
|
|
||||||
}
|
|
||||||
|
|
||||||
onTabClicked: function(index) {
|
|
||||||
root.currentTabIndex = index
|
|
||||||
}
|
|
||||||
|
|
||||||
onActionTriggered: function(index) {
|
|
||||||
let settingsIndex = SettingsData.weatherEnabled ? 3 : 2
|
|
||||||
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 (SettingsData.weatherEnabled && currentIndex === 2) return weatherTab.implicitHeight
|
|
||||||
return overviewTab.implicitHeight
|
|
||||||
}
|
|
||||||
currentIndex: root.currentTabIndex
|
|
||||||
|
|
||||||
OverviewTab {
|
|
||||||
id: overviewTab
|
|
||||||
|
|
||||||
onSwitchToWeatherTab: {
|
|
||||||
if (SettingsData.weatherEnabled) {
|
|
||||||
tabBar.currentIndex = 2
|
|
||||||
tabBar.tabClicked(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSwitchToMediaTab: {
|
|
||||||
tabBar.currentIndex = 1
|
|
||||||
tabBar.tabClicked(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaPlayerTab {
|
|
||||||
id: mediaTab
|
|
||||||
}
|
|
||||||
|
|
||||||
WeatherTab {
|
|
||||||
id: weatherTab
|
|
||||||
visible: SettingsData.weatherEnabled && root.currentTabIndex === 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: dock
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "quickshell:dock"
|
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
property var modelData
|
|
||||||
property var contextMenu
|
|
||||||
property bool autoHide: SettingsData.dockAutoHide
|
|
||||||
property real backgroundTransparency: SettingsData.dockTransparency
|
|
||||||
property bool groupByApp: SettingsData.dockGroupByApp
|
|
||||||
|
|
||||||
property bool contextMenuOpen: (contextMenu && contextMenu.visible && contextMenu.screen === modelData)
|
|
||||||
property bool windowIsFullscreen: {
|
|
||||||
if (!ToplevelManager.activeToplevel) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const activeWindow = ToplevelManager.activeToplevel
|
|
||||||
const fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
|
|
||||||
return fullscreenApps.some(app => activeWindow.appId && activeWindow.appId.toLowerCase().includes(app))
|
|
||||||
}
|
|
||||||
property bool reveal: (!autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen) && !windowIsFullscreen
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onDockTransparencyChanged() {
|
|
||||||
dock.backgroundTransparency = SettingsData.dockTransparency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: modelData
|
|
||||||
visible: SettingsData.showDock
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
bottom: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
}
|
|
||||||
|
|
||||||
margins {
|
|
||||||
left: 0
|
|
||||||
right: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: 100
|
|
||||||
exclusiveZone: autoHide ? -1 : 65 - 16
|
|
||||||
|
|
||||||
mask: Region {
|
|
||||||
item: dockMouseArea
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dockMouseArea
|
|
||||||
property real currentScreen: modelData ? modelData : dock.screen
|
|
||||||
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
|
|
||||||
property real maxDockWidth: Math.min(screenWidth * 0.8, 1200)
|
|
||||||
|
|
||||||
height: dock.reveal ? 65 : 20
|
|
||||||
width: dock.reveal ? Math.min(dockBackground.width + 32, maxDockWidth) : Math.min(Math.max(dockBackground.width + 64, 200), screenWidth * 0.5)
|
|
||||||
anchors {
|
|
||||||
bottom: parent.bottom
|
|
||||||
horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: dockContainer
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
id: dockSlide
|
|
||||||
y: dock.reveal ? 0 : 60
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: dockBackground
|
|
||||||
objectName: "dockBackground"
|
|
||||||
anchors {
|
|
||||||
top: parent.top
|
|
||||||
bottom: parent.bottom
|
|
||||||
horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
width: dockApps.implicitWidth + 12
|
|
||||||
height: parent.height - 8
|
|
||||||
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.bottomMargin: 1
|
|
||||||
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
|
||||||
radius: parent.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
DockApps {
|
|
||||||
id: dockApps
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.bottomMargin: 4
|
|
||||||
|
|
||||||
contextMenu: dock.contextMenu
|
|
||||||
groupByApp: dock.groupByApp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: appTooltip
|
|
||||||
|
|
||||||
property var hoveredButton: {
|
|
||||||
if (!dockApps.children[0]) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const row = dockApps.children[0]
|
|
||||||
let repeater = null
|
|
||||||
for (var i = 0; i < row.children.length; i++) {
|
|
||||||
const child = row.children[i]
|
|
||||||
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
|
|
||||||
repeater = child
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!repeater || !repeater.itemAt) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
for (var i = 0; i < repeater.count; i++) {
|
|
||||||
const item = repeater.itemAt(i)
|
|
||||||
if (item && item.dockButton && item.dockButton.showTooltip) {
|
|
||||||
return item.dockButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
|
|
||||||
|
|
||||||
visible: hoveredButton !== null && tooltipText !== ""
|
|
||||||
width: tooltipLabel.implicitWidth + 24
|
|
||||||
height: tooltipLabel.implicitHeight + 12
|
|
||||||
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
|
|
||||||
y: -height - 8
|
|
||||||
x: hoveredButton ? hoveredButton.mapToItem(dockContainer, hoveredButton.width / 2, 0).x - width / 2 : 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: appTooltip.tooltipText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
clip: false
|
|
||||||
property var appData
|
|
||||||
property var contextMenu: null
|
|
||||||
property var dockApps: null
|
|
||||||
property int index: -1
|
|
||||||
property bool longPressing: false
|
|
||||||
property bool dragging: false
|
|
||||||
property point dragStartPos: Qt.point(0, 0)
|
|
||||||
property point dragOffset: Qt.point(0, 0)
|
|
||||||
property int targetIndex: -1
|
|
||||||
property int originalIndex: -1
|
|
||||||
property bool showWindowTitle: false
|
|
||||||
property string windowTitle: ""
|
|
||||||
property bool isHovered: mouseArea.containsMouse && !dragging
|
|
||||||
property bool showTooltip: mouseArea.containsMouse && !dragging
|
|
||||||
property bool isWindowFocused: {
|
|
||||||
if (!appData) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.type === "window") {
|
|
||||||
const toplevel = getToplevelObject()
|
|
||||||
if (!toplevel) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return toplevel.activated
|
|
||||||
} else if (appData.type === "grouped") {
|
|
||||||
// For grouped apps, check if any window is focused
|
|
||||||
const allToplevels = ToplevelManager.toplevels.values
|
|
||||||
for (let i = 0; i < allToplevels.length; i++) {
|
|
||||||
const toplevel = allToplevels[i]
|
|
||||||
if (toplevel.appId === appData.appId && toplevel.activated) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
property string tooltipText: {
|
|
||||||
if (!appData) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((appData.type === "window" && showWindowTitle) || (appData.type === "grouped" && appData.windowTitle)) {
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
const appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId
|
|
||||||
const title = appData.type === "window" ? windowTitle : appData.windowTitle
|
|
||||||
return appName + (title ? " • " + title : "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!appData.appId) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
function getToplevelObject() {
|
|
||||||
if (!appData) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedToplevels = CompositorService.sortedToplevels
|
|
||||||
if (!sortedToplevels) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.type === "window") {
|
|
||||||
if (appData.uniqueId) {
|
|
||||||
for (var i = 0; i < sortedToplevels.length; i++) {
|
|
||||||
const toplevel = sortedToplevels[i]
|
|
||||||
const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
|
|
||||||
if (checkId === appData.uniqueId) {
|
|
||||||
return toplevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.windowId !== undefined && appData.windowId !== null && appData.windowId >= 0) {
|
|
||||||
if (appData.windowId < sortedToplevels.length) {
|
|
||||||
return sortedToplevels[appData.windowId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (appData.type === "grouped") {
|
|
||||||
if (appData.windowId !== undefined && appData.windowId !== null && appData.windowId >= 0) {
|
|
||||||
if (appData.windowId < sortedToplevels.length) {
|
|
||||||
return sortedToplevels[appData.windowId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGroupedToplevels() {
|
|
||||||
if (!appData || appData.type !== "grouped") {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const toplevels = []
|
|
||||||
const allToplevels = ToplevelManager.toplevels.values
|
|
||||||
for (let i = 0; i < allToplevels.length; i++) {
|
|
||||||
const toplevel = allToplevels[i]
|
|
||||||
if (toplevel.appId === appData.appId) {
|
|
||||||
toplevels.push(toplevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return toplevels
|
|
||||||
}
|
|
||||||
onIsHoveredChanged: {
|
|
||||||
if (isHovered) {
|
|
||||||
exitAnimation.stop()
|
|
||||||
if (!bounceAnimation.running)
|
|
||||||
bounceAnimation.restart()
|
|
||||||
} else {
|
|
||||||
bounceAnimation.stop()
|
|
||||||
exitAnimation.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: bounceAnimation
|
|
||||||
|
|
||||||
running: false
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: translateY
|
|
||||||
property: "y"
|
|
||||||
to: -10
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedAccel
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: translateY
|
|
||||||
property: "y"
|
|
||||||
to: -8
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedDecel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: exitAnimation
|
|
||||||
|
|
||||||
running: false
|
|
||||||
target: translateY
|
|
||||||
property: "y"
|
|
||||||
to: 0
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedDecel
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
|
||||||
border.width: 2
|
|
||||||
border.color: Theme.primary
|
|
||||||
visible: dragging
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: longPressTimer
|
|
||||||
|
|
||||||
interval: 500
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (appData && appData.isPinned) {
|
|
||||||
longPressing = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.bottomMargin: -20
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
onPressed: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton && appData && appData.isPinned) {
|
|
||||||
dragStartPos = Qt.point(mouse.x, mouse.y)
|
|
||||||
longPressTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: mouse => {
|
|
||||||
longPressTimer.stop()
|
|
||||||
if (longPressing) {
|
|
||||||
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) {
|
|
||||||
dockApps.movePinnedApp(originalIndex, targetIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
longPressing = false
|
|
||||||
dragging = false
|
|
||||||
dragOffset = Qt.point(0, 0)
|
|
||||||
targetIndex = -1
|
|
||||||
originalIndex = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (longPressing && !dragging) {
|
|
||||||
const distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2))
|
|
||||||
if (distance > 5) {
|
|
||||||
dragging = true
|
|
||||||
targetIndex = index
|
|
||||||
originalIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dragging) {
|
|
||||||
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y)
|
|
||||||
if (dockApps) {
|
|
||||||
const threshold = 40
|
|
||||||
let newTargetIndex = targetIndex
|
|
||||||
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) {
|
|
||||||
newTargetIndex = targetIndex + 1
|
|
||||||
} else if (dragOffset.x < -threshold && targetIndex > 0) {
|
|
||||||
newTargetIndex = targetIndex - 1
|
|
||||||
}
|
|
||||||
if (newTargetIndex !== targetIndex) {
|
|
||||||
targetIndex = newTargetIndex
|
|
||||||
dragStartPos = Qt.point(mouse.x, mouse.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (!appData || longPressing) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
if (appData.type === "pinned") {
|
|
||||||
if (appData && appData.appId) {
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
if (desktopEntry) {
|
|
||||||
AppUsageHistoryData.addAppUsage({
|
|
||||||
"id": appData.appId,
|
|
||||||
"name": desktopEntry.name || appData.appId,
|
|
||||||
"icon": desktopEntry.icon || "",
|
|
||||||
"exec": desktopEntry.exec || "",
|
|
||||||
"comment": desktopEntry.comment || ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
SessionService.launchDesktopEntry(desktopEntry)
|
|
||||||
}
|
|
||||||
} else if (appData.type === "window") {
|
|
||||||
const toplevel = getToplevelObject()
|
|
||||||
if (toplevel) {
|
|
||||||
toplevel.activate()
|
|
||||||
}
|
|
||||||
} else if (appData.type === "grouped") {
|
|
||||||
if (appData.windowCount === 0) {
|
|
||||||
if (appData && appData.appId) {
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
if (desktopEntry) {
|
|
||||||
AppUsageHistoryData.addAppUsage({
|
|
||||||
"id": appData.appId,
|
|
||||||
"name": desktopEntry.name || appData.appId,
|
|
||||||
"icon": desktopEntry.icon || "",
|
|
||||||
"exec": desktopEntry.exec || "",
|
|
||||||
"comment": desktopEntry.comment || ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
SessionService.launchDesktopEntry(desktopEntry)
|
|
||||||
}
|
|
||||||
} else if (appData.windowCount === 1) {
|
|
||||||
// For single window, activate directly
|
|
||||||
const toplevel = getToplevelObject()
|
|
||||||
if (toplevel) {
|
|
||||||
console.log("Activating grouped app window:", appData.windowTitle)
|
|
||||||
toplevel.activate()
|
|
||||||
} else {
|
|
||||||
console.warn("No toplevel found for grouped app")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For multiple windows, show context menu (hide pin option for left-click)
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.showForButton(root, appData, 65, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.MiddleButton) {
|
|
||||||
if (appData && appData.appId) {
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
if (desktopEntry) {
|
|
||||||
AppUsageHistoryData.addAppUsage({
|
|
||||||
"id": appData.appId,
|
|
||||||
"name": desktopEntry.name || appData.appId,
|
|
||||||
"icon": desktopEntry.icon || "",
|
|
||||||
"exec": desktopEntry.exec || "",
|
|
||||||
"comment": desktopEntry.comment || ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
SessionService.launchDesktopEntry(desktopEntry)
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
if (contextMenu && appData) {
|
|
||||||
console.log("Right-clicked on app:", appData.appId, "type:", appData.type, "windowCount:", appData.windowCount || 0)
|
|
||||||
contextMenu.showForButton(root, appData, 40, false)
|
|
||||||
} else {
|
|
||||||
console.warn("No context menu or appData available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
implicitSize: 40
|
|
||||||
source: {
|
|
||||||
if (appData.appId === "__SEPARATOR__") {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
const moddedId = Paths.moddedAppId(appData.appId)
|
|
||||||
if (moddedId.toLowerCase().includes("steam_app")) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId)
|
|
||||||
return desktopEntry && desktopEntry.icon ? Quickshell.iconPath(desktopEntry.icon, true) : ""
|
|
||||||
}
|
|
||||||
mipmap: true
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
size: 40
|
|
||||||
name: "sports_esports"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
visible: {
|
|
||||||
if (!appData || !appData.appId || appData.appId === "__SEPARATOR__") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const moddedId = Paths.moddedAppId(appData.appId)
|
|
||||||
return moddedId.toLowerCase().includes("steam_app")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: iconImg.status !== Image.Ready
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
if (!appData || !appData.appId) {
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
if (desktopEntry && desktopEntry.name) {
|
|
||||||
return desktopEntry.name.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
return appData.appId.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indicator for running/focused state
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: -2
|
|
||||||
spacing: 2
|
|
||||||
visible: appData && (appData.isRunning || appData.type === "window" || (appData.type === "grouped" && appData.windowCount > 0))
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
if (!appData) return 0
|
|
||||||
if (appData.type === "grouped") {
|
|
||||||
return Math.min(appData.windowCount, 4)
|
|
||||||
} else if (appData.type === "window" || appData.isRunning) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: appData && appData.type === "grouped" && appData.windowCount > 1 ? 4 : 8
|
|
||||||
height: 2
|
|
||||||
radius: 1
|
|
||||||
color: {
|
|
||||||
if (!appData) {
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.type !== "grouped" || appData.windowCount === 1) {
|
|
||||||
if (isWindowFocused) {
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.type === "grouped" && appData.windowCount > 1) {
|
|
||||||
const groupToplevels = getGroupedToplevels()
|
|
||||||
if (index < groupToplevels.length && groupToplevels[index].activated) {
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
id: translateY
|
|
||||||
|
|
||||||
y: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
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
|
|
||||||
|
|
||||||
implicitWidth: row.width
|
|
||||||
implicitHeight: row.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: row
|
|
||||||
spacing: 2
|
|
||||||
anchors.centerIn: parent
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: repeater
|
|
||||||
model: ListModel {
|
|
||||||
id: dockModel
|
|
||||||
|
|
||||||
Component.onCompleted: updateModel()
|
|
||||||
|
|
||||||
function updateModel() {
|
|
||||||
clear()
|
|
||||||
|
|
||||||
const items = []
|
|
||||||
const pinnedApps = [...(SessionData.pinnedApps || [])]
|
|
||||||
const sortedToplevels = CompositorService.sortedToplevels
|
|
||||||
|
|
||||||
if (root.groupByApp) {
|
|
||||||
// Group windows by appId
|
|
||||||
const appGroups = new Map()
|
|
||||||
|
|
||||||
// Add pinned apps first (even if they have no windows)
|
|
||||||
pinnedApps.forEach(appId => {
|
|
||||||
appGroups.set(appId, {
|
|
||||||
appId: appId,
|
|
||||||
isPinned: true,
|
|
||||||
windows: []
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Group all running windows by appId
|
|
||||||
sortedToplevels.forEach((toplevel, index) => {
|
|
||||||
const appId = toplevel.appId || "unknown"
|
|
||||||
if (!appGroups.has(appId)) {
|
|
||||||
appGroups.set(appId, {
|
|
||||||
appId: appId,
|
|
||||||
isPinned: false,
|
|
||||||
windows: []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const title = toplevel.title || "(Unnamed)"
|
|
||||||
const truncatedTitle = title.length > 50 ? title.substring(0, 47) + "..." : title
|
|
||||||
const uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index
|
|
||||||
|
|
||||||
appGroups.get(appId).windows.push({
|
|
||||||
windowId: index,
|
|
||||||
windowTitle: truncatedTitle,
|
|
||||||
uniqueId: uniqueId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sort groups: pinned first, then unpinned
|
|
||||||
const pinnedGroups = []
|
|
||||||
const unpinnedGroups = []
|
|
||||||
|
|
||||||
Array.from(appGroups.entries()).forEach(([appId, group]) => {
|
|
||||||
// For grouped apps, just show the first window info but track all windows
|
|
||||||
const firstWindow = group.windows.length > 0 ? group.windows[0] : null
|
|
||||||
|
|
||||||
const item = {
|
|
||||||
"type": "grouped",
|
|
||||||
"appId": appId,
|
|
||||||
"windowId": firstWindow ? firstWindow.windowId : -1,
|
|
||||||
"windowTitle": firstWindow ? firstWindow.windowTitle : "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": group.isPinned,
|
|
||||||
"isRunning": group.windows.length > 0,
|
|
||||||
"windowCount": group.windows.length,
|
|
||||||
"uniqueId": firstWindow ? firstWindow.uniqueId : "",
|
|
||||||
"allWindows": group.windows
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.isPinned) {
|
|
||||||
pinnedGroups.push(item)
|
|
||||||
} else {
|
|
||||||
unpinnedGroups.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add items in order
|
|
||||||
pinnedGroups.forEach(item => items.push(item))
|
|
||||||
|
|
||||||
// Add separator if needed
|
|
||||||
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
|
||||||
items.push({
|
|
||||||
"type": "separator",
|
|
||||||
"appId": "__SEPARATOR__",
|
|
||||||
"windowId": -1,
|
|
||||||
"windowTitle": "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": false,
|
|
||||||
"isRunning": false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
unpinnedGroups.forEach(item => items.push(item))
|
|
||||||
root.pinnedAppCount = pinnedGroups.length
|
|
||||||
} else {
|
|
||||||
pinnedApps.forEach(appId => {
|
|
||||||
items.push({
|
|
||||||
"type": "pinned",
|
|
||||||
"appId": appId,
|
|
||||||
"windowId": -1,
|
|
||||||
"windowTitle": "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": true,
|
|
||||||
"isRunning": false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
root.pinnedAppCount = pinnedApps.length
|
|
||||||
|
|
||||||
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
|
|
||||||
items.push({
|
|
||||||
"type": "separator",
|
|
||||||
"appId": "__SEPARATOR__",
|
|
||||||
"windowId": -1,
|
|
||||||
"windowTitle": "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": false,
|
|
||||||
"isRunning": false,
|
|
||||||
"isFocused": false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sortedToplevels.forEach((toplevel, index) => {
|
|
||||||
const title = toplevel.title || "(Unnamed)"
|
|
||||||
const truncatedTitle = title.length > 50 ? title.substring(0, 47) + "..." : title
|
|
||||||
const uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
"type": "window",
|
|
||||||
"appId": toplevel.appId,
|
|
||||||
"windowId": index,
|
|
||||||
"windowTitle": truncatedTitle,
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": false,
|
|
||||||
"isRunning": true,
|
|
||||||
"uniqueId": uniqueId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
items.forEach(item => append(item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateItem
|
|
||||||
property alias dockButton: button
|
|
||||||
|
|
||||||
width: model.type === "separator" ? 16 : 40
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: model.type === "separator"
|
|
||||||
width: 2
|
|
||||||
height: 20
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
|
||||||
radius: 1
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
DockAppButton {
|
|
||||||
id: button
|
|
||||||
visible: model.type !== "separator"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
appData: model
|
|
||||||
contextMenu: root.contextMenu
|
|
||||||
dockApps: root
|
|
||||||
index: model.index
|
|
||||||
|
|
||||||
// Override tooltip for windows to show window title
|
|
||||||
showWindowTitle: model.type === "window" || model.type === "grouped"
|
|
||||||
windowTitle: model.windowTitle || ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: CompositorService
|
|
||||||
function onSortedToplevelsChanged() {
|
|
||||||
dockModel.updateModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onPinnedAppsChanged() {
|
|
||||||
dockModel.updateModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onGroupByAppChanged: {
|
|
||||||
dockModel.updateModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: customButtonKeyboard
|
|
||||||
circular: false
|
|
||||||
property string text: ""
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
property bool isShift: false
|
|
||||||
color: Theme.surface
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: contentItem
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: parent.text
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
font.weight: Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
property Item target
|
|
||||||
height: 60 * 5
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
color: Theme.widgetBackground
|
|
||||||
|
|
||||||
property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
|
|
||||||
property double columnSpacing: 0.02 * height // vertical spacing between keyboard
|
|
||||||
property bool shift: false //Boolean for the shift state
|
|
||||||
property bool symbols: false //Boolean for the symbol state
|
|
||||||
property double columns: 10 // Number of column
|
|
||||||
property double rows: 4 // Number of row
|
|
||||||
|
|
||||||
property string strShift: '\u2191' // UPWARDS ARROW unicode
|
|
||||||
property string strBackspace: "Backspace"
|
|
||||||
|
|
||||||
property var modelKeyboard: {
|
|
||||||
"row_1": [
|
|
||||||
{
|
|
||||||
text: 'q',
|
|
||||||
symbol: '1',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'w',
|
|
||||||
symbol: '2',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'e',
|
|
||||||
symbol: '3',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'r',
|
|
||||||
symbol: '4',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 't',
|
|
||||||
symbol: '5',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'y',
|
|
||||||
symbol: '6',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'u',
|
|
||||||
symbol: '7',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'i',
|
|
||||||
symbol: '8',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'o',
|
|
||||||
symbol: '9',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'p',
|
|
||||||
symbol: '0',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"row_2": [
|
|
||||||
{
|
|
||||||
text: 'a',
|
|
||||||
symbol: '-',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 's',
|
|
||||||
symbol: '/',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'd',
|
|
||||||
symbol: ':',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'f',
|
|
||||||
symbol: ';',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'g',
|
|
||||||
symbol: '(',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'h',
|
|
||||||
symbol: ')',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'j',
|
|
||||||
symbol: '€',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'k',
|
|
||||||
symbol: '&',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'l',
|
|
||||||
symbol: '@',
|
|
||||||
width: 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"row_3": [
|
|
||||||
{
|
|
||||||
text: strShift,
|
|
||||||
symbol: strShift,
|
|
||||||
width: 1.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'z',
|
|
||||||
symbol: '.',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'x',
|
|
||||||
symbol: ',',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'c',
|
|
||||||
symbol: '?',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'v',
|
|
||||||
symbol: '!',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'b',
|
|
||||||
symbol: "'",
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'n',
|
|
||||||
symbol: "%",
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'm',
|
|
||||||
symbol: '"',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "'",
|
|
||||||
symbol: "*",
|
|
||||||
width: 1.5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"row_4": [
|
|
||||||
{
|
|
||||||
text: '123',
|
|
||||||
symbol: 'ABC',
|
|
||||||
width: 1.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: ' ',
|
|
||||||
symbol: ' ',
|
|
||||||
width: 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '.',
|
|
||||||
symbol: '.',
|
|
||||||
width: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: strBackspace,
|
|
||||||
symbol: strBackspace,
|
|
||||||
width: 1.5
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
//Here is the corresponding table between the ascii and the key event
|
|
||||||
property var tableKeyEvent: {
|
|
||||||
"_0": Qt.Key_0,
|
|
||||||
"_1": Qt.Key_1,
|
|
||||||
"_2": Qt.Key_2,
|
|
||||||
"_3": Qt.Key_3,
|
|
||||||
"_4": Qt.Key_4,
|
|
||||||
"_5": Qt.Key_5,
|
|
||||||
"_6": Qt.Key_6,
|
|
||||||
"_7": Qt.Key_7,
|
|
||||||
"_8": Qt.Key_8,
|
|
||||||
"_9": Qt.Key_9,
|
|
||||||
"_a": Qt.Key_A,
|
|
||||||
"_b": Qt.Key_B,
|
|
||||||
"_c": Qt.Key_C,
|
|
||||||
"_d": Qt.Key_D,
|
|
||||||
"_e": Qt.Key_E,
|
|
||||||
"_f": Qt.Key_F,
|
|
||||||
"_g": Qt.Key_G,
|
|
||||||
"_h": Qt.Key_H,
|
|
||||||
"_i": Qt.Key_I,
|
|
||||||
"_j": Qt.Key_J,
|
|
||||||
"_k": Qt.Key_K,
|
|
||||||
"_l": Qt.Key_L,
|
|
||||||
"_m": Qt.Key_M,
|
|
||||||
"_n": Qt.Key_N,
|
|
||||||
"_o": Qt.Key_O,
|
|
||||||
"_p": Qt.Key_P,
|
|
||||||
"_q": Qt.Key_Q,
|
|
||||||
"_r": Qt.Key_R,
|
|
||||||
"_s": Qt.Key_S,
|
|
||||||
"_t": Qt.Key_T,
|
|
||||||
"_u": Qt.Key_U,
|
|
||||||
"_v": Qt.Key_V,
|
|
||||||
"_w": Qt.Key_W,
|
|
||||||
"_x": Qt.Key_X,
|
|
||||||
"_y": Qt.Key_Y,
|
|
||||||
"_z": Qt.Key_Z,
|
|
||||||
"_\u2190": Qt.Key_Backspace,
|
|
||||||
"_return": Qt.Key_Return,
|
|
||||||
"_ ": Qt.Key_Space,
|
|
||||||
"_-": Qt.Key_Minus,
|
|
||||||
"_/": Qt.Key_Slash,
|
|
||||||
"_:": Qt.Key_Colon,
|
|
||||||
"_;": Qt.Key_Semicolon,
|
|
||||||
"_(": Qt.Key_BracketLeft,
|
|
||||||
"_)": Qt.Key_BracketRight,
|
|
||||||
"_€": parseInt("20ac", 16) // I didn't find the appropriate Qt event so I used the hex format
|
|
||||||
,
|
|
||||||
"_&": Qt.Key_Ampersand,
|
|
||||||
"_@": Qt.Key_At,
|
|
||||||
'_"': Qt.Key_QuoteDbl,
|
|
||||||
"_.": Qt.Key_Period,
|
|
||||||
"_,": Qt.Key_Comma,
|
|
||||||
"_?": Qt.Key_Question,
|
|
||||||
"_!": Qt.Key_Exclam,
|
|
||||||
"_'": Qt.Key_Apostrophe,
|
|
||||||
"_%": Qt.Key_Percent,
|
|
||||||
"_*": Qt.Key_Asterisk
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: keyboard_container
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 5
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 5
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: 5
|
|
||||||
|
|
||||||
//One column which contains 5 rows
|
|
||||||
Column {
|
|
||||||
spacing: columnSpacing
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: row_1
|
|
||||||
spacing: rowSpacing
|
|
||||||
Repeater {
|
|
||||||
model: modelKeyboard["row_1"]
|
|
||||||
delegate: CustomButtonKeyboard {
|
|
||||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
|
||||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
|
||||||
height: keyboard_container.height / rows - columnSpacing
|
|
||||||
|
|
||||||
onClicked: root.clicked(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
id: row_2
|
|
||||||
spacing: rowSpacing
|
|
||||||
Repeater {
|
|
||||||
model: modelKeyboard["row_2"]
|
|
||||||
delegate: CustomButtonKeyboard {
|
|
||||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
|
||||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
|
||||||
height: keyboard_container.height / rows - columnSpacing
|
|
||||||
|
|
||||||
onClicked: root.clicked(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
id: row_3
|
|
||||||
spacing: rowSpacing
|
|
||||||
Repeater {
|
|
||||||
model: modelKeyboard["row_3"]
|
|
||||||
delegate: CustomButtonKeyboard {
|
|
||||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
|
||||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
|
||||||
height: keyboard_container.height / rows - columnSpacing
|
|
||||||
isShift: shift && text === strShift
|
|
||||||
|
|
||||||
onClicked: root.clicked(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
id: row_4
|
|
||||||
spacing: rowSpacing
|
|
||||||
Repeater {
|
|
||||||
model: modelKeyboard["row_4"]
|
|
||||||
delegate: CustomButtonKeyboard {
|
|
||||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
|
||||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
|
||||||
height: keyboard_container.height / rows - columnSpacing
|
|
||||||
|
|
||||||
onClicked: root.clicked(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
signal clicked(string text)
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onClicked(text) {
|
|
||||||
if (!keyboard_controller.target)
|
|
||||||
return;
|
|
||||||
if (text === strShift) {
|
|
||||||
root.shift = !root.shift; // toggle shift
|
|
||||||
} else if (text === '123') {
|
|
||||||
root.symbols = true;
|
|
||||||
} else if (text === 'ABC') {
|
|
||||||
root.symbols = false;
|
|
||||||
} else {
|
|
||||||
// insert text into target
|
|
||||||
if (text === strBackspace) {
|
|
||||||
var current = keyboard_controller.target.text;
|
|
||||||
keyboard_controller.target.text = current.slice(0, current.length - 1);
|
|
||||||
} else {
|
|
||||||
// normal character
|
|
||||||
var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase() : text);
|
|
||||||
var current = keyboard_controller.target.text;
|
|
||||||
var cursorPos = keyboard_controller.target.cursorPosition;
|
|
||||||
keyboard_controller.target.text = current.slice(0, cursorPos) + charToInsert + current.slice(cursorPos);
|
|
||||||
keyboard_controller.target.cursorPosition = cursorPos + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// shift is momentary
|
|
||||||
if (root.shift && text !== strShift)
|
|
||||||
root.shift = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property string sid: Quickshell.env("XDG_SESSION_ID") || "self"
|
|
||||||
property string sessionPath: ""
|
|
||||||
|
|
||||||
function activate() {
|
|
||||||
loader.activeAsync = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
getSessionPath.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
lockStateMonitor.running = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: IdleService
|
|
||||||
function onLockRequested() {
|
|
||||||
console.log("Lock: Received lock request from IdleService")
|
|
||||||
activate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: getSessionPath
|
|
||||||
command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const match = text.match(/objectpath '([^']+)'/)
|
|
||||||
if (match) {
|
|
||||||
root.sessionPath = match[1]
|
|
||||||
console.log("Found session path:", root.sessionPath)
|
|
||||||
checkCurrentLockState.running = true
|
|
||||||
lockStateMonitor.running = true
|
|
||||||
} else {
|
|
||||||
console.warn("Could not determine session path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("Failed to get session path, exit code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: checkCurrentLockState
|
|
||||||
command: root.sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : []
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text.includes("true")) {
|
|
||||||
console.log("Session is locked on startup, activating lock screen")
|
|
||||||
loader.activeAsync = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("Failed to check initial lock state, exit code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: lockStateMonitor
|
|
||||||
command: root.sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1"] : []
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
|
|
||||||
onRead: line => {
|
|
||||||
if (line.includes(root.sessionPath)) {
|
|
||||||
if (line.includes("org.freedesktop.login1.Session.Lock")) {
|
|
||||||
console.log("login1: Lock signal received -> show lock")
|
|
||||||
loader.activeAsync = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (line.includes("org.freedesktop.login1.Session.Unlock")) {
|
|
||||||
console.log("login1: Unlock signal received -> hide lock")
|
|
||||||
loader.active = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (line.includes("LockedHint") && line.includes("true")) {
|
|
||||||
console.log("login1: LockedHint=true -> show lock")
|
|
||||||
loader.activeAsync = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (line.includes("LockedHint") && line.includes("false")) {
|
|
||||||
console.log("login1: LockedHint=false -> hide lock")
|
|
||||||
loader.active = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (line.includes("PrepareForSleep") &&
|
|
||||||
line.includes("true") &&
|
|
||||||
SessionData.lockBeforeSuspend) {
|
|
||||||
loader.activeAsync = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("gdbus monitor failed, exit code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: loader
|
|
||||||
|
|
||||||
WlSessionLock {
|
|
||||||
id: sessionLock
|
|
||||||
|
|
||||||
property bool unlocked: false
|
|
||||||
property string sharedPasswordBuffer: ""
|
|
||||||
|
|
||||||
locked: true
|
|
||||||
|
|
||||||
onLockedChanged: {
|
|
||||||
if (!locked) {
|
|
||||||
loader.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LockSurface {
|
|
||||||
id: lockSurface
|
|
||||||
lock: sessionLock
|
|
||||||
sharedPasswordBuffer: sessionLock.sharedPasswordBuffer
|
|
||||||
onPasswordChanged: newPassword => {
|
|
||||||
sessionLock.sharedPasswordBuffer = newPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LockScreenDemo {
|
|
||||||
id: demoWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "lock"
|
|
||||||
|
|
||||||
function lock() {
|
|
||||||
console.log("Lock screen requested via IPC")
|
|
||||||
loader.activeAsync = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function demo() {
|
|
||||||
console.log("Lock screen DEMO mode requested via IPC")
|
|
||||||
demoWindow.showDemo()
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLocked(): bool {
|
|
||||||
return loader.active
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
WlSessionLockSurface {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property WlSessionLock lock
|
|
||||||
required property string sharedPasswordBuffer
|
|
||||||
|
|
||||||
signal passwordChanged(string newPassword)
|
|
||||||
|
|
||||||
readonly property bool locked: lock && !lock.locked
|
|
||||||
|
|
||||||
function unlock(): void {
|
|
||||||
lock.locked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
sourceComponent: LockScreenContent {
|
|
||||||
demoMode: false
|
|
||||||
passwordBuffer: root.sharedPasswordBuffer
|
|
||||||
screenName: root.screen?.name ?? ""
|
|
||||||
onUnlockRequested: root.unlock()
|
|
||||||
onPasswordBufferChanged: {
|
|
||||||
if (root.sharedPasswordBuffer !== passwordBuffer) {
|
|
||||||
root.passwordChanged(passwordBuffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: displaysTab
|
|
||||||
|
|
||||||
property var variantComponents: [{
|
|
||||||
"id": "topBar",
|
|
||||||
"name": "Top Bar",
|
|
||||||
"description": "System bar with widgets and system information",
|
|
||||||
"icon": "toolbar"
|
|
||||||
}, {
|
|
||||||
"id": "dock",
|
|
||||||
"name": "Application Dock",
|
|
||||||
"description": "Bottom dock for pinned and running applications",
|
|
||||||
"icon": "dock"
|
|
||||||
}, {
|
|
||||||
"id": "notifications",
|
|
||||||
"name": "Notification Popups",
|
|
||||||
"description": "Notification toast popups",
|
|
||||||
"icon": "notifications"
|
|
||||||
}, {
|
|
||||||
"id": "wallpaper",
|
|
||||||
"name": "Wallpaper",
|
|
||||||
"description": "Desktop background images",
|
|
||||||
"icon": "wallpaper"
|
|
||||||
}, {
|
|
||||||
"id": "osd",
|
|
||||||
"name": "On-Screen Displays",
|
|
||||||
"description": "Volume, brightness, and other system OSDs",
|
|
||||||
"icon": "picture_in_picture"
|
|
||||||
}, {
|
|
||||||
"id": "toast",
|
|
||||||
"name": "Toast Messages",
|
|
||||||
"description": "System toast notifications",
|
|
||||||
"icon": "campaign"
|
|
||||||
}, {
|
|
||||||
"id": "notepad",
|
|
||||||
"name": "Notepad Slideout",
|
|
||||||
"description": "Quick note-taking slideout panel",
|
|
||||||
"icon": "sticky_note_2"
|
|
||||||
}, {
|
|
||||||
"id": "systemTray",
|
|
||||||
"name": "System Tray",
|
|
||||||
"description": "System tray icons",
|
|
||||||
"icon": "notifications"
|
|
||||||
}]
|
|
||||||
|
|
||||||
function getScreenPreferences(componentId) {
|
|
||||||
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setScreenPreferences(componentId, screenNames) {
|
|
||||||
var prefs = SettingsData.screenPreferences || {
|
|
||||||
};
|
|
||||||
prefs[componentId] = screenNames;
|
|
||||||
SettingsData.setScreenPreferences(prefs);
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: screensInfoSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: screensInfoSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "monitor"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Connected Displays"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Configure which displays show shell components"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Available Screens (" + Quickshell.screens.length + ")"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Quickshell.screens
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: screenRow.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: screenRow
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "desktop_windows"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM * 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS / 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.width + "×" + modelData.height
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "•"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.model || "Unknown Model"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: displaysTab.variantComponents
|
|
||||||
|
|
||||||
delegate: StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: componentSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: componentSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: modelData.icon
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.description
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show on screens:"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
property string componentId: modelData.id
|
|
||||||
property var selectedScreens: displaysTab.getScreenPreferences(componentId)
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "All displays"
|
|
||||||
description: "Show on all connected displays"
|
|
||||||
checked: parent.selectedScreens.includes("all")
|
|
||||||
onToggled: (checked) => {
|
|
||||||
if (checked)
|
|
||||||
displaysTab.setScreenPreferences(parent.componentId, ["all"]);
|
|
||||||
else
|
|
||||||
displaysTab.setScreenPreferences(parent.componentId, []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.2
|
|
||||||
visible: !parent.selectedScreens.includes("all")
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: !parent.selectedScreens.includes("all")
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Quickshell.screens
|
|
||||||
|
|
||||||
delegate: DankToggle {
|
|
||||||
property string screenName: modelData.name
|
|
||||||
property string componentId: parent.parent.componentId
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
text: screenName
|
|
||||||
description: modelData.width + "×" + modelData.height + " • " + (modelData.model || "Unknown Model")
|
|
||||||
checked: {
|
|
||||||
var prefs = displaysTab.getScreenPreferences(componentId);
|
|
||||||
return !prefs.includes("all") && prefs.includes(screenName);
|
|
||||||
}
|
|
||||||
onToggled: (checked) => {
|
|
||||||
var currentPrefs = displaysTab.getScreenPreferences(componentId);
|
|
||||||
if (currentPrefs.includes("all"))
|
|
||||||
currentPrefs = [];
|
|
||||||
|
|
||||||
var newPrefs = currentPrefs.slice();
|
|
||||||
if (checked) {
|
|
||||||
if (!newPrefs.includes(screenName))
|
|
||||||
newPrefs.push(screenName);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
var index = newPrefs.indexOf(screenName);
|
|
||||||
if (index > -1)
|
|
||||||
newPrefs.splice(index, 1);
|
|
||||||
|
|
||||||
}
|
|
||||||
displaysTab.setScreenPreferences(componentId, newPrefs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
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
|
|
||||||
|
|
||||||
// Enable Dock
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: enableDockSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: enableDockSection
|
|
||||||
|
|
||||||
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: "Show Dock"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Display a dock at the bottom of the screen with pinned and running applications"
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-hide Dock
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: autoHideSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
visible: SettingsData.showDock
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: autoHideSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
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: "Auto-hide Dock"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Hide the dock when not in use and reveal it when hovering near the bottom of the screen"
|
|
||||||
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.setDockAutoHide(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group by App
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: groupByAppSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
visible: SettingsData.showDock
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: 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: "Group by App"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "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.setDockGroupByApp(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dock Transparency Section
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: transparencySection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
visible: SettingsData.showDock
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: 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: "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.surfaceContainerHigh
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setDockTransparency(
|
|
||||||
newValue / 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: recentAppsTab
|
|
||||||
|
|
||||||
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: launchPrefixSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: 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: "Launch Prefix"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "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: SessionData.launchPrefix
|
|
||||||
placeholderText: "Enter launch prefix (e.g., 'uwsm-app')"
|
|
||||||
onTextEdited: {
|
|
||||||
SessionData.setLaunchPrefix(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: 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: "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 = {}
|
|
||||||
SettingsData.saveSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Apps are ordered by usage frequency, then last used, then alphabetically."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: rankedAppsList
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: recentlyUsedSection.rankedAppsModel
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: rankedAppsList.width
|
|
||||||
height: 48
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
|
||||||
Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.1)
|
|
||||||
border.width: 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 "Last launched just now"
|
|
||||||
|
|
||||||
if (diffMins < 60)
|
|
||||||
return "Last launched " + diffMins + " minute"
|
|
||||||
+ (diffMins === 1 ? "" : "s") + " ago"
|
|
||||||
|
|
||||||
if (diffHours < 24)
|
|
||||||
return "Last launched " + diffHours + " hour"
|
|
||||||
+ (diffHours === 1 ? "" : "s") + " ago"
|
|
||||||
|
|
||||||
if (diffDays < 7)
|
|
||||||
return "Last launched " + diffDays + " day"
|
|
||||||
+ (diffDays === 1 ? "" : "s") + " ago"
|
|
||||||
|
|
||||||
return "Last launched " + date.toLocaleDateString()
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
circular: true
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: 16
|
|
||||||
iconColor: Theme.error
|
|
||||||
onClicked: {
|
|
||||||
var currentRanking = Object.assign(
|
|
||||||
{},
|
|
||||||
AppUsageHistoryData.appUsageRanking
|
|
||||||
|| {})
|
|
||||||
delete currentRanking[modelData.id]
|
|
||||||
AppUsageHistoryData.appUsageRanking = currentRanking
|
|
||||||
SettingsData.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,380 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: timeTab
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// Time Format
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: timeSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: timeSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "schedule"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- toggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "24-Hour Format"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Use 24-hour time format instead of 12-hour AM/PM"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: toggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.use24HourClock
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setClockFormat(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date Format Section
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: dateSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: dateSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "calendar_today"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Date Format"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
text: "Top Bar Format"
|
|
||||||
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
|
|
||||||
currentValue: {
|
|
||||||
if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0) {
|
|
||||||
return "System Default"
|
|
||||||
}
|
|
||||||
// Find matching preset or show "Custom"
|
|
||||||
const presets = [{
|
|
||||||
"format": "ddd d",
|
|
||||||
"label": "Day Date"
|
|
||||||
}, {
|
|
||||||
"format": "ddd MMM d",
|
|
||||||
"label": "Day Month Date"
|
|
||||||
}, {
|
|
||||||
"format": "MMM d",
|
|
||||||
"label": "Month Date"
|
|
||||||
}, {
|
|
||||||
"format": "M/d",
|
|
||||||
"label": "Numeric (M/D)"
|
|
||||||
}, {
|
|
||||||
"format": "d/M",
|
|
||||||
"label": "Numeric (D/M)"
|
|
||||||
}, {
|
|
||||||
"format": "ddd d MMM yyyy",
|
|
||||||
"label": "Full with Year"
|
|
||||||
}, {
|
|
||||||
"format": "yyyy-MM-dd",
|
|
||||||
"label": "ISO Date"
|
|
||||||
}, {
|
|
||||||
"format": "dddd, MMMM d",
|
|
||||||
"label": "Full Day & Month"
|
|
||||||
}]
|
|
||||||
const match = presets.find(p => {
|
|
||||||
return p.format
|
|
||||||
=== SettingsData.clockDateFormat
|
|
||||||
})
|
|
||||||
return match ? match.label : "Custom: " + SettingsData.clockDateFormat
|
|
||||||
}
|
|
||||||
options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
|
|
||||||
onValueChanged: value => {
|
|
||||||
const formatMap = {
|
|
||||||
"System Default": "",
|
|
||||||
"Day Date": "ddd d",
|
|
||||||
"Day Month Date": "ddd MMM d",
|
|
||||||
"Month Date": "MMM d",
|
|
||||||
"Numeric (M/D)": "M/d",
|
|
||||||
"Numeric (D/M)": "d/M",
|
|
||||||
"Full with Year": "ddd d MMM yyyy",
|
|
||||||
"ISO Date": "yyyy-MM-dd",
|
|
||||||
"Full Day & Month": "dddd, MMMM d"
|
|
||||||
}
|
|
||||||
if (value === "Custom...") {
|
|
||||||
customFormatInput.visible = true
|
|
||||||
} else {
|
|
||||||
customFormatInput.visible = false
|
|
||||||
SettingsData.setClockDateFormat(
|
|
||||||
formatMap[value])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
text: "Lock Screen Format"
|
|
||||||
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))
|
|
||||||
currentValue: {
|
|
||||||
if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0) {
|
|
||||||
return "System Default"
|
|
||||||
}
|
|
||||||
// Find matching preset or show "Custom"
|
|
||||||
const presets = [{
|
|
||||||
"format": "ddd d",
|
|
||||||
"label": "Day Date"
|
|
||||||
}, {
|
|
||||||
"format": "ddd MMM d",
|
|
||||||
"label": "Day Month Date"
|
|
||||||
}, {
|
|
||||||
"format": "MMM d",
|
|
||||||
"label": "Month Date"
|
|
||||||
}, {
|
|
||||||
"format": "M/d",
|
|
||||||
"label": "Numeric (M/D)"
|
|
||||||
}, {
|
|
||||||
"format": "d/M",
|
|
||||||
"label": "Numeric (D/M)"
|
|
||||||
}, {
|
|
||||||
"format": "ddd d MMM yyyy",
|
|
||||||
"label": "Full with Year"
|
|
||||||
}, {
|
|
||||||
"format": "yyyy-MM-dd",
|
|
||||||
"label": "ISO Date"
|
|
||||||
}, {
|
|
||||||
"format": "dddd, MMMM d",
|
|
||||||
"label": "Full Day & Month"
|
|
||||||
}]
|
|
||||||
const match = presets.find(p => {
|
|
||||||
return p.format
|
|
||||||
=== SettingsData.lockDateFormat
|
|
||||||
})
|
|
||||||
return match ? match.label : "Custom: " + SettingsData.lockDateFormat
|
|
||||||
}
|
|
||||||
options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
|
|
||||||
onValueChanged: value => {
|
|
||||||
const formatMap = {
|
|
||||||
"System Default": "",
|
|
||||||
"Day Date": "ddd d",
|
|
||||||
"Day Month Date": "ddd MMM d",
|
|
||||||
"Month Date": "MMM d",
|
|
||||||
"Numeric (M/D)": "M/d",
|
|
||||||
"Numeric (D/M)": "d/M",
|
|
||||||
"Full with Year": "ddd d MMM yyyy",
|
|
||||||
"ISO Date": "yyyy-MM-dd",
|
|
||||||
"Full Day & Month": "dddd, MMMM d"
|
|
||||||
}
|
|
||||||
if (value === "Custom...") {
|
|
||||||
customLockFormatInput.visible = true
|
|
||||||
} else {
|
|
||||||
customLockFormatInput.visible = false
|
|
||||||
SettingsData.setLockDateFormat(
|
|
||||||
formatMap[value])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: customFormatInput
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
visible: false
|
|
||||||
placeholderText: "Enter custom top bar format (e.g., ddd MMM d)"
|
|
||||||
text: SettingsData.clockDateFormat
|
|
||||||
onTextChanged: {
|
|
||||||
if (visible && text)
|
|
||||||
SettingsData.setClockDateFormat(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: customLockFormatInput
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
visible: false
|
|
||||||
placeholderText: "Enter custom lock screen format (e.g., dddd, MMMM d)"
|
|
||||||
text: SettingsData.lockDateFormat
|
|
||||||
onTextChanged: {
|
|
||||||
if (visible && text)
|
|
||||||
SettingsData.setLockDateFormat(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: formatHelp.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.1)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: formatHelp
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Format Legend"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingL) / 2
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• d - Day (1-31)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• dd - Day (01-31)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• ddd - Day name (Mon)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• dddd - Day name (Monday)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• M - Month (1-12)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingL) / 2
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• MM - Month (01-12)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• MMM - Month (Jan)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• MMMM - Month (January)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• yy - Year (24)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• yyyy - Year (2024)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,390 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: weatherTab
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// Enable Weather
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: enableWeatherSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: enableWeatherSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "cloud"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- enableToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Enable Weather"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show weather information in top bar and control center"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: enableToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.weatherEnabled
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setWeatherEnabled(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temperature Unit
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: temperatureSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
visible: SettingsData.weatherEnabled
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: temperatureSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "thermostat"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- temperatureToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Use Fahrenheit"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Use Fahrenheit instead of Celsius for temperature"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: temperatureToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.useFahrenheit
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setTemperatureUnit(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Location Settings
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: locationSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
visible: SettingsData.weatherEnabled
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: locationSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "location_on"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- autoLocationToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Auto Location"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Automatically determine your location using your IP address"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: autoLocationToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.useAutoLocation
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setAutoLocation(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: !SettingsData.useAutoLocation
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Custom Location"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Latitude"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: latitudeInput
|
|
||||||
width: parent.width
|
|
||||||
height: 48
|
|
||||||
placeholderText: "40.7128"
|
|
||||||
backgroundColor: Theme.surfaceVariant
|
|
||||||
normalBorderColor: Theme.primarySelected
|
|
||||||
focusedBorderColor: Theme.primary
|
|
||||||
keyNavigationTab: longitudeInput
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (SettingsData.weatherCoordinates) {
|
|
||||||
const coords = SettingsData.weatherCoordinates.split(',')
|
|
||||||
if (coords.length > 0) {
|
|
||||||
text = coords[0].trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onWeatherCoordinatesChanged() {
|
|
||||||
if (SettingsData.weatherCoordinates) {
|
|
||||||
const coords = SettingsData.weatherCoordinates.split(',')
|
|
||||||
if (coords.length > 0) {
|
|
||||||
latitudeInput.text = coords[0].trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextEdited: {
|
|
||||||
if (text && longitudeInput.text) {
|
|
||||||
const coords = text + "," + longitudeInput.text
|
|
||||||
SettingsData.weatherCoordinates = coords
|
|
||||||
SettingsData.saveSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Longitude"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: longitudeInput
|
|
||||||
width: parent.width
|
|
||||||
height: 48
|
|
||||||
placeholderText: "-74.0060"
|
|
||||||
backgroundColor: Theme.surfaceVariant
|
|
||||||
normalBorderColor: Theme.primarySelected
|
|
||||||
focusedBorderColor: Theme.primary
|
|
||||||
keyNavigationTab: locationSearchInput
|
|
||||||
keyNavigationBacktab: latitudeInput
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (SettingsData.weatherCoordinates) {
|
|
||||||
const coords = SettingsData.weatherCoordinates.split(',')
|
|
||||||
if (coords.length > 1) {
|
|
||||||
text = coords[1].trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onWeatherCoordinatesChanged() {
|
|
||||||
if (SettingsData.weatherCoordinates) {
|
|
||||||
const coords = SettingsData.weatherCoordinates.split(',')
|
|
||||||
if (coords.length > 1) {
|
|
||||||
longitudeInput.text = coords[1].trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextEdited: {
|
|
||||||
if (text && latitudeInput.text) {
|
|
||||||
const coords = latitudeInput.text + "," + text
|
|
||||||
SettingsData.weatherCoordinates = coords
|
|
||||||
SettingsData.saveSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Location Search"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankLocationSearch {
|
|
||||||
id: locationSearchInput
|
|
||||||
width: parent.width
|
|
||||||
currentLocation: ""
|
|
||||||
placeholderText: "New York, NY"
|
|
||||||
keyNavigationBacktab: longitudeInput
|
|
||||||
onLocationSelected: (displayName, coordinates) => {
|
|
||||||
SettingsData.setWeatherLocation(displayName, coordinates)
|
|
||||||
|
|
||||||
const coords = coordinates.split(',')
|
|
||||||
if (coords.length >= 2) {
|
|
||||||
latitudeInput.text = coords[0].trim()
|
|
||||||
longitudeInput.text = coords[1].trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Services.UPower
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: battery
|
|
||||||
|
|
||||||
property bool batteryPopupVisible: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal toggleBatteryPopup()
|
|
||||||
|
|
||||||
width: batteryContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = batteryArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
visible: true
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: batteryContent
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: SettingsData.topBarNoBackground ? 1 : 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BatteryService.getBatteryIcon()
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: {
|
|
||||||
if (!BatteryService.batteryAvailable) {
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
|
||||||
return Theme.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
|
|
||||||
return Theme.primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `${BatteryService.batteryLevel}%`
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: BatteryService.batteryAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: batteryArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
toggleBatteryPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: batteryTooltip
|
|
||||||
|
|
||||||
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.widgetBaseBackgroundColor
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
visible: batteryArea.containsMouse && !batteryPopupVisible
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
opacity: batteryArea.containsMouse ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipText
|
|
||||||
|
|
||||||
text: {
|
|
||||||
if (!BatteryService.batteryAvailable) {
|
|
||||||
if (typeof PowerProfiles === "undefined") {
|
|
||||||
return "Power Management";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (PowerProfiles.profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "Power Profile: Power Saver";
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "Power Profile: Performance";
|
|
||||||
default:
|
|
||||||
return "Power Profile: Balanced";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const status = BatteryService.batteryStatus;
|
|
||||||
const level = `${BatteryService.batteryLevel}%`;
|
|
||||||
const time = BatteryService.formatTimeRemaining();
|
|
||||||
if (time !== "Unknown") {
|
|
||||||
return `${status} • ${level} • ${time}`;
|
|
||||||
} else {
|
|
||||||
return `${status} • ${level}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool compactMode: false
|
|
||||||
property string section: "center"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
|
|
||||||
signal clockClicked
|
|
||||||
|
|
||||||
width: clockRow.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = clockMouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: clockRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
|
||||||
return systemClock?.date?.toLocaleTimeString(Qt.locale(), format)
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium - 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "•"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outlineButton
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !SettingsData.clockCompactMode
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
|
|
||||||
return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
return systemClock?.date?.toLocaleDateString(Qt.locale(), "ddd d")
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium - 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !SettingsData.clockCompactMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemClock {
|
|
||||||
id: systemClock
|
|
||||||
precision: SystemClock.Seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clockMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0)
|
|
||||||
const currentScreen = parentScreen || Screen
|
|
||||||
const screenX = currentScreen.x || 0
|
|
||||||
const relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen)
|
|
||||||
}
|
|
||||||
root.clockClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
width: colorPickerIcon.width + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = colorPickerArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: colorPickerIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "palette"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: colorPickerArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: colorPickerArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
console.log("Color picker button clicked!")
|
|
||||||
root.colorPickerRequested();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal to notify TopBar to open color picker
|
|
||||||
signal colorPickerRequested()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property var widgetData: null
|
|
||||||
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
|
|
||||||
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
|
|
||||||
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
width: controlIndicators.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = controlCenterArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: controlIndicators
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: networkIcon
|
|
||||||
|
|
||||||
name: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return "sync";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "lan";
|
|
||||||
}
|
|
||||||
|
|
||||||
return NetworkService.wifiSignalIcon;
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return Theme.primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showNetworkIcon
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: bluetoothIcon
|
|
||||||
|
|
||||||
name: "bluetooth"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: BluetoothService.enabled ? Theme.primary : Theme.outlineButton
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: audioIcon.implicitWidth + 4
|
|
||||||
height: audioIcon.implicitHeight + 4
|
|
||||||
color: "transparent"
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showAudioIcon
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: audioIcon
|
|
||||||
|
|
||||||
name: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
|
|
||||||
return "volume_off";
|
|
||||||
} else if (AudioService.sink.audio.volume * 100 < 33) {
|
|
||||||
return "volume_down";
|
|
||||||
} else {
|
|
||||||
return "volume_up";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "volume_up";
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: audioWheelArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
onWheel: function(wheelEvent) {
|
|
||||||
let delta = wheelEvent.angleDelta.y;
|
|
||||||
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
|
|
||||||
let newVolume;
|
|
||||||
if (delta > 0) {
|
|
||||||
newVolume = Math.min(100, currentVolume + 5);
|
|
||||||
} else {
|
|
||||||
newVolume = Math.max(0, currentVolume - 5);
|
|
||||||
}
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.muted = false;
|
|
||||||
AudioService.sink.audio.volume = newVolume / 100;
|
|
||||||
AudioService.volumeChanged();
|
|
||||||
}
|
|
||||||
wheelEvent.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "mic"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: false // TODO: Add mic detection
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback settings icon when all other icons are hidden
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: controlCenterArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
root.clicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showPercentage: true
|
|
||||||
property bool showIcon: true
|
|
||||||
property var toggleProcessList
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: cpuContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = cpuArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["cpu"]);
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["cpu"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cpuArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
DgopService.setSortBy("cpu");
|
|
||||||
if (root.toggleProcessList) {
|
|
||||||
root.toggleProcessList();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: cpuContent
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "memory"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (DgopService.cpuUsage > 80) {
|
|
||||||
return Theme.tempDanger;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DgopService.cpuUsage > 60) {
|
|
||||||
return Theme.tempWarning;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null || DgopService.cpuUsage === 0) {
|
|
||||||
return "--%";
|
|
||||||
}
|
|
||||||
|
|
||||||
return DgopService.cpuUsage.toFixed(0) + "%";
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
elide: Text.ElideNone
|
|
||||||
|
|
||||||
StyledTextMetrics {
|
|
||||||
id: cpuBaseline
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
text: "100%"
|
|
||||||
}
|
|
||||||
|
|
||||||
width: Math.max(cpuBaseline.width, paintedWidth)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showPercentage: true
|
|
||||||
property bool showIcon: true
|
|
||||||
property var toggleProcessList
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: cpuTempContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = cpuTempArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["cpu"]);
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["cpu"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cpuTempArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
DgopService.setSortBy("cpu");
|
|
||||||
if (root.toggleProcessList) {
|
|
||||||
root.toggleProcessList();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: cpuTempContent
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "memory"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (DgopService.cpuTemperature > 85) {
|
|
||||||
return Theme.tempDanger;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DgopService.cpuTemperature > 69) {
|
|
||||||
return Theme.tempWarning;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) {
|
|
||||||
return "--°";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.round(DgopService.cpuTemperature) + "°";
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
elide: Text.ElideNone
|
|
||||||
|
|
||||||
StyledTextMetrics {
|
|
||||||
id: tempBaseline
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
text: "100°"
|
|
||||||
}
|
|
||||||
|
|
||||||
width: Math.max(tempBaseline.width, paintedWidth)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var widgetData: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property string mountPath: (widgetData && widgetData.mountPath !== undefined) ? widgetData.mountPath : "/"
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
property var selectedMount: {
|
|
||||||
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force re-evaluation when mountPath changes
|
|
||||||
const currentMountPath = root.mountPath || "/"
|
|
||||||
|
|
||||||
// First try to find exact match
|
|
||||||
for (let i = 0; i < DgopService.diskMounts.length; i++) {
|
|
||||||
if (DgopService.diskMounts[i].mount === currentMountPath) {
|
|
||||||
return DgopService.diskMounts[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to root
|
|
||||||
for (let i = 0; i < DgopService.diskMounts.length; i++) {
|
|
||||||
if (DgopService.diskMounts[i].mount === "/") {
|
|
||||||
return DgopService.diskMounts[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort - first mount
|
|
||||||
return DgopService.diskMounts[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
property real diskUsagePercent: {
|
|
||||||
if (!selectedMount || !selectedMount.percent) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
const percentStr = selectedMount.percent.replace("%", "")
|
|
||||||
return parseFloat(percentStr) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
width: diskContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["diskmounts"])
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["diskmounts"])
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onWidgetDataChanged() {
|
|
||||||
// Force property re-evaluation by triggering change detection
|
|
||||||
root.mountPath = Qt.binding(() => {
|
|
||||||
return (root.widgetData && root.widgetData.mountPath !== undefined) ? root.widgetData.mountPath : "/"
|
|
||||||
})
|
|
||||||
|
|
||||||
root.selectedMount = Qt.binding(() => {
|
|
||||||
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentMountPath = root.mountPath || "/"
|
|
||||||
|
|
||||||
// First try to find exact match
|
|
||||||
for (let i = 0; i < DgopService.diskMounts.length; i++) {
|
|
||||||
if (DgopService.diskMounts[i].mount === currentMountPath) {
|
|
||||||
return DgopService.diskMounts[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to root
|
|
||||||
for (let i = 0; i < DgopService.diskMounts.length; i++) {
|
|
||||||
if (DgopService.diskMounts[i].mount === "/") {
|
|
||||||
return DgopService.diskMounts[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort - first mount
|
|
||||||
return DgopService.diskMounts[0] || null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
target: SettingsData
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: diskContent
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "storage"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (root.diskUsagePercent > 90) {
|
|
||||||
return Theme.tempDanger
|
|
||||||
}
|
|
||||||
if (root.diskUsagePercent > 75) {
|
|
||||||
return Theme.tempWarning
|
|
||||||
}
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!root.selectedMount) {
|
|
||||||
return "--"
|
|
||||||
}
|
|
||||||
return root.selectedMount.mount
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
elide: Text.ElideNone
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
|
|
||||||
return "--%"
|
|
||||||
}
|
|
||||||
return root.diskUsagePercent.toFixed(0) + "%"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
elide: Text.ElideNone
|
|
||||||
|
|
||||||
StyledTextMetrics {
|
|
||||||
id: diskBaseline
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
text: "100%"
|
|
||||||
}
|
|
||||||
|
|
||||||
width: Math.max(diskBaseline.width, paintedWidth)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool compactMode: SettingsData.focusedWindowCompactMode
|
|
||||||
property int availableWidth: 400
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
readonly property int baseWidth: contentRow.implicitWidth + horizontalPadding * 2
|
|
||||||
readonly property int maxNormalWidth: 456
|
|
||||||
readonly property int maxCompactWidth: 288
|
|
||||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
|
||||||
readonly property bool hasWindowsOnCurrentWorkspace: {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
let currentWorkspaceId = null
|
|
||||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
|
||||||
const ws = NiriService.allWorkspaces[i]
|
|
||||||
if (ws.is_focused) {
|
|
||||||
currentWorkspaceId = ws.id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentWorkspaceId) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId)
|
|
||||||
return workspaceWindows.length > 0 && activeWindow && activeWindow.title
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
|
|
||||||
const activeHyprToplevel = hyprlandToplevels.find(t => t.wayland === activeWindow)
|
|
||||||
|
|
||||||
if (!activeHyprToplevel || !activeHyprToplevel.workspace) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return activeHyprToplevel.workspace.id === Hyprland.focusedWorkspace.id
|
|
||||||
}
|
|
||||||
|
|
||||||
return activeWindow && activeWindow.title
|
|
||||||
}
|
|
||||||
|
|
||||||
width: !hasWindowsOnCurrentWorkspace ? 0 : (compactMode ? Math.min(baseWidth, maxCompactWidth) : Math.min(baseWidth, maxNormalWidth))
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (!activeWindow || !activeWindow.title) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
clip: true
|
|
||||||
visible: hasWindowsOnCurrentWorkspace
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: appText
|
|
||||||
|
|
||||||
text: {
|
|
||||||
if (!activeWindow || !activeWindow.appId) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(activeWindow.appId);
|
|
||||||
return desktopEntry && desktopEntry.name ? desktopEntry.name : activeWindow.appId;
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
width: Math.min(implicitWidth, compactMode ? 80 : 180)
|
|
||||||
visible: !compactMode && text.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "•"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outlineButton
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !compactMode && appText.text && titleText.text
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: titleText
|
|
||||||
|
|
||||||
text: {
|
|
||||||
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
|
||||||
const appName = appText.text;
|
|
||||||
if (!title || !appName) {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove app name from end of title if it exists there
|
|
||||||
if (title.endsWith(" - " + appName)) {
|
|
||||||
return title.substring(0, title.length - (" - " + appName).length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.endsWith(appName)) {
|
|
||||||
return title.substring(0, title.length - appName.length).replace(/ - $/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
width: Math.min(implicitWidth, compactMode ? 280 : 250)
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showPercentage: true
|
|
||||||
property bool showIcon: true
|
|
||||||
property var toggleProcessList
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property var widgetData: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex !== undefined) ? widgetData.selectedGpuIndex : 0
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
property real displayTemp: {
|
|
||||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedGpuIndex >= 0 && selectedGpuIndex < DgopService.availableGpus.length) {
|
|
||||||
return DgopService.availableGpus[selectedGpuIndex].temperature || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateWidgetPciId(pciId) {
|
|
||||||
// Find and update this widget's pciId in the settings
|
|
||||||
const sections = ["left", "center", "right"];
|
|
||||||
for (let s = 0; s < sections.length; s++) {
|
|
||||||
const sectionId = sections[s];
|
|
||||||
let widgets = [];
|
|
||||||
if (sectionId === "left") {
|
|
||||||
widgets = SettingsData.topBarLeftWidgets.slice();
|
|
||||||
} else if (sectionId === "center") {
|
|
||||||
widgets = SettingsData.topBarCenterWidgets.slice();
|
|
||||||
} else if (sectionId === "right") {
|
|
||||||
widgets = SettingsData.topBarRightWidgets.slice();
|
|
||||||
}
|
|
||||||
for (let i = 0; i < widgets.length; i++) {
|
|
||||||
const widget = widgets[i];
|
|
||||||
if (typeof widget === "object" && widget.id === "gpuTemp" && (!widget.pciId || widget.pciId === "")) {
|
|
||||||
widgets[i] = {
|
|
||||||
"id": widget.id,
|
|
||||||
"enabled": widget.enabled !== undefined ? widget.enabled : true,
|
|
||||||
"selectedGpuIndex": 0,
|
|
||||||
"pciId": pciId
|
|
||||||
};
|
|
||||||
if (sectionId === "left") {
|
|
||||||
SettingsData.setTopBarLeftWidgets(widgets);
|
|
||||||
} else if (sectionId === "center") {
|
|
||||||
SettingsData.setTopBarCenterWidgets(widgets);
|
|
||||||
} else if (sectionId === "right") {
|
|
||||||
SettingsData.setTopBarRightWidgets(widgets);
|
|
||||||
}
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: gpuTempContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = gpuArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["gpu"]);
|
|
||||||
console.log("GpuTemperature widget - pciId:", widgetData ? widgetData.pciId : "no widgetData", "selectedGpuIndex:", widgetData ? widgetData.selectedGpuIndex : "no widgetData");
|
|
||||||
// Add this widget's PCI ID to the service
|
|
||||||
if (widgetData && widgetData.pciId) {
|
|
||||||
console.log("Adding GPU PCI ID to service:", widgetData.pciId);
|
|
||||||
DgopService.addGpuPciId(widgetData.pciId);
|
|
||||||
} else {
|
|
||||||
console.log("No PCI ID in widget data, starting auto-detection");
|
|
||||||
// No PCI ID saved, auto-detect and save the first GPU
|
|
||||||
autoSaveTimer.running = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["gpu"]);
|
|
||||||
// Remove this widget's PCI ID from the service
|
|
||||||
if (widgetData && widgetData.pciId) {
|
|
||||||
DgopService.removeGpuPciId(widgetData.pciId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onWidgetDataChanged() {
|
|
||||||
// Force property re-evaluation by triggering change detection
|
|
||||||
root.selectedGpuIndex = Qt.binding(() => {
|
|
||||||
return (root.widgetData && root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
target: SettingsData
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gpuArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
DgopService.setSortBy("cpu");
|
|
||||||
if (root.toggleProcessList) {
|
|
||||||
root.toggleProcessList();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: gpuTempContent
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "auto_awesome_mosaic"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (root.displayTemp > 80) {
|
|
||||||
return Theme.tempDanger;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.displayTemp > 65) {
|
|
||||||
return Theme.tempWarning;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) {
|
|
||||||
return "--°";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.round(root.displayTemp) + "°";
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
elide: Text.ElideNone
|
|
||||||
|
|
||||||
StyledTextMetrics {
|
|
||||||
id: gpuTempBaseline
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
text: "100°"
|
|
||||||
}
|
|
||||||
|
|
||||||
width: Math.max(gpuTempBaseline.width, paintedWidth)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: autoSaveTimer
|
|
||||||
|
|
||||||
interval: 100
|
|
||||||
running: false
|
|
||||||
onTriggered: {
|
|
||||||
if (DgopService.availableGpus && DgopService.availableGpus.length > 0) {
|
|
||||||
const firstGpu = DgopService.availableGpus[0];
|
|
||||||
if (firstGpu && firstGpu.pciId) {
|
|
||||||
// Save the first GPU's PCI ID to this widget's settings
|
|
||||||
updateWidgetPciId(firstGpu.pciId);
|
|
||||||
DgopService.addGpuPciId(firstGpu.pciId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: idleIcon.width + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: idleIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
SessionService.toggleIdleInhibit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
property string currentLayout: ""
|
|
||||||
property string hyprlandKeyboard: ""
|
|
||||||
|
|
||||||
width: contentRow.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
NiriService.cycleKeyboardLayout()
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
Quickshell.execDetached([
|
|
||||||
"hyprctl",
|
|
||||||
"switchxkblayout",
|
|
||||||
root.hyprlandKeyboard,
|
|
||||||
"next"
|
|
||||||
])
|
|
||||||
updateLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: currentLayout
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: hyprlandLayoutProcess
|
|
||||||
running: false
|
|
||||||
command: ["hyprctl", "-j", "devices"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(text)
|
|
||||||
// Find the main keyboard and get its active keymap
|
|
||||||
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
|
|
||||||
root.hyprlandKeyboard = mainKeyboard.name
|
|
||||||
if (mainKeyboard && mainKeyboard.active_keymap) {
|
|
||||||
root.currentLayout = mainKeyboard.active_keymap
|
|
||||||
} else {
|
|
||||||
root.currentLayout = "Unknown"
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
root.currentLayout = "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: updateTimer
|
|
||||||
interval: 1000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
updateLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
updateLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLayout() {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
root.currentLayout = NiriService.getCurrentKeyboardLayoutName()
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
hyprlandLayoutProcess.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "left"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
width: Theme.iconSize + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: launcherArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
root.clicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: launcherContent
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = launcherArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemLogo {
|
|
||||||
visible: SettingsData.useOSLogo
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: Theme.iconSize - 3
|
|
||||||
height: Theme.iconSize - 3
|
|
||||||
colorOverride: SettingsData.osLogoColorOverride
|
|
||||||
brightnessOverride: SettingsData.osLogoBrightness
|
|
||||||
contrastOverride: SettingsData.osLogoContrast
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
visible: !SettingsData.useOSLogo
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "apps"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
|
||||||
readonly property bool playerAvailable: activePlayer !== null
|
|
||||||
property bool compactMode: false
|
|
||||||
readonly property int textWidth: {
|
|
||||||
switch (SettingsData.mediaSize) {
|
|
||||||
case 0:
|
|
||||||
return 0; // No text in small mode
|
|
||||||
case 2:
|
|
||||||
return 180; // Large text area
|
|
||||||
default:
|
|
||||||
return 120; // Medium text area
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly property int currentContentWidth: {
|
|
||||||
// Calculate actual content width:
|
|
||||||
// AudioViz (20) + spacing + [text + spacing] + controls (prev:20 + spacing + play:24 + spacing + next:20) + padding
|
|
||||||
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
|
|
||||||
// ~72px total
|
|
||||||
const audioVizWidth = 20;
|
|
||||||
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
|
|
||||||
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0) + horizontalPadding * 2;
|
|
||||||
}
|
|
||||||
property string section: "center"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
name: "shown"
|
|
||||||
when: playerAvailable
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
opacity: 1
|
|
||||||
width: currentContentWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "hidden"
|
|
||||||
when: !playerAvailable
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
opacity: 0
|
|
||||||
width: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
]
|
|
||||||
transitions: [
|
|
||||||
Transition {
|
|
||||||
from: "shown"
|
|
||||||
to: "hidden"
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
PauseAnimation {
|
|
||||||
duration: 500
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "opacity,width"
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
Transition {
|
|
||||||
from: "hidden"
|
|
||||||
to: "shown"
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "opacity,width"
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: mediaRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: mediaInfo
|
|
||||||
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
AudioVisualization {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: textContainer
|
|
||||||
|
|
||||||
property string displayText: {
|
|
||||||
if (!activePlayer || !activePlayer.trackTitle) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
let identity = activePlayer.identity || "";
|
|
||||||
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari");
|
|
||||||
let title = "";
|
|
||||||
let subtitle = "";
|
|
||||||
if (isWebMedia && activePlayer.trackTitle) {
|
|
||||||
title = activePlayer.trackTitle;
|
|
||||||
subtitle = activePlayer.trackArtist || identity;
|
|
||||||
} else {
|
|
||||||
title = activePlayer.trackTitle || "Unknown Track";
|
|
||||||
subtitle = activePlayer.trackArtist || "";
|
|
||||||
}
|
|
||||||
return subtitle.length > 0 ? title + " • " + subtitle : title;
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: textWidth
|
|
||||||
height: 20
|
|
||||||
visible: SettingsData.mediaSize > 0
|
|
||||||
clip: true
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: mediaText
|
|
||||||
|
|
||||||
property bool needsScrolling: implicitWidth > textContainer.width
|
|
||||||
property real scrollOffset: 0
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: textContainer.displayText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
x: needsScrolling ? -scrollOffset : 0
|
|
||||||
onTextChanged: {
|
|
||||||
scrollOffset = 0;
|
|
||||||
scrollAnimation.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: scrollAnimation
|
|
||||||
|
|
||||||
running: mediaText.needsScrolling && textContainer.visible
|
|
||||||
loops: Animation.Infinite
|
|
||||||
|
|
||||||
PauseAnimation {
|
|
||||||
duration: 2000
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: mediaText
|
|
||||||
property: "scrollOffset"
|
|
||||||
from: 0
|
|
||||||
to: mediaText.implicitWidth - textContainer.width + 5
|
|
||||||
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
}
|
|
||||||
|
|
||||||
PauseAnimation {
|
|
||||||
duration: 2000
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: mediaText
|
|
||||||
property: "scrollOffset"
|
|
||||||
to: 0
|
|
||||||
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.playerAvailable && root.opacity > 0 && root.width > 0 && textContainer.visible
|
|
||||||
hoverEnabled: enabled
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onPressed: {
|
|
||||||
if (root.popupTarget && root.popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = root.parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
root.popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, root.width, root.section, currentScreen);
|
|
||||||
}
|
|
||||||
root.clicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
radius: 10
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: prevArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
visible: root.playerAvailable
|
|
||||||
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_previous"
|
|
||||||
size: 12
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: prevArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.playerAvailable && root.width > 0
|
|
||||||
hoverEnabled: enabled
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: {
|
|
||||||
if (activePlayer) {
|
|
||||||
activePlayer.previous();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
radius: 12
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
|
|
||||||
visible: root.playerAvailable
|
|
||||||
opacity: activePlayer ? 1 : 0.3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
|
|
||||||
size: 14
|
|
||||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.playerAvailable && root.width > 0
|
|
||||||
hoverEnabled: enabled
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: {
|
|
||||||
if (activePlayer) {
|
|
||||||
activePlayer.togglePlaying();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
radius: 10
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: nextArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
visible: playerAvailable
|
|
||||||
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_next"
|
|
||||||
size: 12
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nextArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.playerAvailable && root.width > 0
|
|
||||||
hoverEnabled: enabled
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: {
|
|
||||||
if (activePlayer) {
|
|
||||||
activePlayer.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int availableWidth: 400
|
|
||||||
readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2
|
|
||||||
readonly property int maxNormalWidth: 456
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
function formatNetworkSpeed(bytesPerSec) {
|
|
||||||
if (bytesPerSec < 1024) {
|
|
||||||
return bytesPerSec.toFixed(0) + " B/s";
|
|
||||||
} else if (bytesPerSec < 1024 * 1024) {
|
|
||||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
|
||||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
|
||||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
|
||||||
} else {
|
|
||||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: contentRow.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = networkArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["network"]);
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["network"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: networkArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "network_check"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "↓"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.info
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
elide: Text.ElideNone
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
|
|
||||||
StyledTextMetrics {
|
|
||||||
id: rxBaseline
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
text: "88.8 MB/s"
|
|
||||||
}
|
|
||||||
|
|
||||||
width: Math.max(rxBaseline.width, paintedWidth)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "↑"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.error
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
elide: Text.ElideNone
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
|
|
||||||
StyledTextMetrics {
|
|
||||||
id: txBaseline
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
text: "88.8 MB/s"
|
|
||||||
}
|
|
||||||
|
|
||||||
width: Math.max(txBaseline.width, paintedWidth)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
width: notepadIcon.width + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = notepadArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: notepadIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "assignment"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: notepadArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 6
|
|
||||||
height: 6
|
|
||||||
radius: 3
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.rightMargin: SettingsData.topBarNoBackground ? 0 : 4
|
|
||||||
anchors.topMargin: SettingsData.topBarNoBackground ? 0 : 4
|
|
||||||
visible: NotepadStorageService.tabs && NotepadStorageService.tabs.length > 0
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: notepadArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.clicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool hasUnread: false
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
width: notificationIcon.width + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = notificationArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: notificationIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: SessionData.doNotDisturb ? Theme.error : (notificationArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 8
|
|
||||||
height: 8
|
|
||||||
radius: 4
|
|
||||||
color: Theme.error
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.rightMargin: SettingsData.topBarNoBackground ? 0 : 6
|
|
||||||
anchors.topMargin: SettingsData.topBarNoBackground ? 0 : 6
|
|
||||||
visible: root.hasUnread
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: notificationArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
root.clicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
readonly property bool hasActivePrivacy: PrivacyService.anyPrivacyActive
|
|
||||||
readonly property int activeCount: PrivacyService.microphoneActive + PrivacyService.cameraActive + PrivacyService.screensharingActive
|
|
||||||
readonly property real contentWidth: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
|
|
||||||
|
|
||||||
width: hasActivePrivacy ? (contentWidth + horizontalPadding * 2) : 0
|
|
||||||
height: hasActivePrivacy ? widgetHeight : 0
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
visible: hasActivePrivacy
|
|
||||||
opacity: hasActivePrivacy ? 1 : 0
|
|
||||||
enabled: hasActivePrivacy
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Qt.rgba(privacyArea.containsMouse ? Theme.errorPressed.r : Theme.errorHover.r, privacyArea.containsMouse ? Theme.errorPressed.g : Theme.errorHover.g, privacyArea.containsMouse ? Theme.errorPressed.b : Theme.errorHover.b, (privacyArea.containsMouse ? Theme.errorPressed.a : Theme.errorHover.a) * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
// Privacy indicator click handler
|
|
||||||
|
|
||||||
id: privacyArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: hasActivePrivacy
|
|
||||||
enabled: hasActivePrivacy
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: hasActivePrivacy
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
visible: PrivacyService.microphoneActive
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "mic"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.error
|
|
||||||
filled: true
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
visible: PrivacyService.cameraActive
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "camera_video"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
filled: true
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 6
|
|
||||||
height: 6
|
|
||||||
radius: 3
|
|
||||||
color: Theme.error
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.topMargin: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
visible: PrivacyService.screensharingActive
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "screen_share"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.warning
|
|
||||||
filled: true
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: tooltip
|
|
||||||
|
|
||||||
width: tooltipText.contentWidth + Theme.spacingM * 2
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
visible: false
|
|
||||||
opacity: privacyArea.containsMouse && hasActivePrivacy ? 1 : 0
|
|
||||||
z: 100
|
|
||||||
x: (parent.width - width) / 2
|
|
||||||
y: -height - Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: PrivacyService.getPrivacySummary()
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 8
|
|
||||||
height: 8
|
|
||||||
color: parent.color
|
|
||||||
border.color: parent.border.color
|
|
||||||
border.width: parent.border.width
|
|
||||||
rotation: 45
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.top: parent.bottom
|
|
||||||
anchors.topMargin: -4
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
enabled: hasActivePrivacy && root.visible
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
enabled: hasActivePrivacy && visible
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showPercentage: true
|
|
||||||
property bool showIcon: true
|
|
||||||
property var toggleProcessList
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: ramContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = ramArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["memory"]);
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["memory"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: ramArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
DgopService.setSortBy("memory");
|
|
||||||
if (root.toggleProcessList) {
|
|
||||||
root.toggleProcessList();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: ramContent
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "developer_board"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (DgopService.memoryUsage > 90) {
|
|
||||||
return Theme.tempDanger;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DgopService.memoryUsage > 75) {
|
|
||||||
return Theme.tempWarning;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) {
|
|
||||||
return "--%";
|
|
||||||
}
|
|
||||||
|
|
||||||
return DgopService.memoryUsage.toFixed(0) + "%";
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
elide: Text.ElideNone
|
|
||||||
|
|
||||||
StyledTextMetrics {
|
|
||||||
id: ramBaseline
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
text: "100%"
|
|
||||||
}
|
|
||||||
|
|
||||||
width: Math.max(ramBaseline.width, paintedWidth)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string section: "left"
|
|
||||||
property var parentScreen
|
|
||||||
property var hoveredItem: null
|
|
||||||
property var topBar: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
// The visual root for this window
|
|
||||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
|
||||||
readonly property var sortedToplevels: {
|
|
||||||
if (SettingsData.runningAppsCurrentWorkspace) {
|
|
||||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen.name);
|
|
||||||
}
|
|
||||||
return CompositorService.sortedToplevels;
|
|
||||||
}
|
|
||||||
readonly property int windowCount: sortedToplevels.length
|
|
||||||
readonly property int calculatedWidth: {
|
|
||||||
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: calculatedWidth
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
visible: windowCount > 0
|
|
||||||
clip: false
|
|
||||||
color: {
|
|
||||||
if (windowCount === 0) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
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 (let i = 0; i < windows.length; i++) {
|
|
||||||
if (windows[i].activated) {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextIndex;
|
|
||||||
if (deltaY < 0) {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = 0;
|
|
||||||
} else {
|
|
||||||
nextIndex = (currentIndex + 1) % 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 (let 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: windowRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: windowRepeater
|
|
||||||
|
|
||||||
model: sortedToplevels
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateItem
|
|
||||||
|
|
||||||
property bool isFocused: modelData.activated
|
|
||||||
property string appId: modelData.appId || ""
|
|
||||||
property string windowTitle: modelData.title || "(Unnamed)"
|
|
||||||
property var toplevelObject: modelData
|
|
||||||
property string tooltipText: {
|
|
||||||
let appName = "Unknown";
|
|
||||||
if (appId) {
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appId);
|
|
||||||
appName = desktopEntry
|
|
||||||
&& desktopEntry.name ? desktopEntry.name : appId;
|
|
||||||
}
|
|
||||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
|
||||||
}
|
|
||||||
|
|
||||||
width: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: 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 - 18) / 2 : Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
source: {
|
|
||||||
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 - 18) / 2 : Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
size: 18
|
|
||||||
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: {
|
|
||||||
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
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.fontSizeMedium - 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
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 (toplevelObject) {
|
|
||||||
toplevelObject.activate();
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hideTooltip();
|
|
||||||
}
|
|
||||||
tooltipLoader.active = false;
|
|
||||||
|
|
||||||
windowContextMenuLoader.active = true;
|
|
||||||
if (windowContextMenuLoader.item) {
|
|
||||||
windowContextMenuLoader.item.currentWindow = toplevelObject;
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0);
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0;
|
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
const yPos = Theme.barHeight + SettingsData.topBarSpacing - 7;
|
|
||||||
windowContextMenuLoader.item.showAt(relativeX, yPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onEntered: {
|
|
||||||
root.hoveredItem = delegateItem;
|
|
||||||
const globalPos = delegateItem.mapToGlobal(
|
|
||||||
delegateItem.width / 2, delegateItem.height);
|
|
||||||
tooltipLoader.active = true;
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
const tooltipY = Theme.barHeight
|
|
||||||
+ SettingsData.topBarSpacing + Theme.spacingXS;
|
|
||||||
tooltipLoader.item.showTooltip(
|
|
||||||
delegateItem.tooltipText, globalPos.x,
|
|
||||||
tooltipY, root.parentScreen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
if (root.hoveredItem === delegateItem) {
|
|
||||||
root.hoveredItem = null;
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hideTooltip();
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltipLoader.active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: tooltipLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
sourceComponent: RunningAppsTooltip {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: windowContextMenuLoader
|
|
||||||
active: false
|
|
||||||
sourceComponent: PanelWindow {
|
|
||||||
id: contextMenuWindow
|
|
||||||
|
|
||||||
property var currentWindow: null
|
|
||||||
property bool isVisible: false
|
|
||||||
property point anchorPos: Qt.point(0, 0)
|
|
||||||
|
|
||||||
function showAt(x, y) {
|
|
||||||
screen = root.parentScreen;
|
|
||||||
anchorPos = Qt.point(x, y);
|
|
||||||
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: {
|
|
||||||
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: contextMenuWindow.anchorPos.y
|
|
||||||
width: 100
|
|
||||||
height: 32
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
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: "Close"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: closeMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (contextMenuWindow.currentWindow) {
|
|
||||||
contextMenuWindow.currentWindow.close();
|
|
||||||
}
|
|
||||||
contextMenuWindow.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string tooltipText: ""
|
|
||||||
property real targetX: 0
|
|
||||||
property real targetY: 0
|
|
||||||
property var targetScreen: null
|
|
||||||
|
|
||||||
function showTooltip(text, x, y, screen) {
|
|
||||||
tooltipText = text;
|
|
||||||
targetScreen = screen;
|
|
||||||
const screenX = screen ? screen.x : 0;
|
|
||||||
targetX = x - screenX;
|
|
||||||
targetY = y;
|
|
||||||
visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideTooltip() {
|
|
||||||
visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: targetScreen
|
|
||||||
implicitWidth: Math.min(300, Math.max(120, textContent.implicitWidth + Theme.spacingM * 2))
|
|
||||||
implicitHeight: textContent.implicitHeight + Theme.spacingS * 2
|
|
||||||
color: "transparent"
|
|
||||||
visible: false
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
}
|
|
||||||
|
|
||||||
margins {
|
|
||||||
left: Math.round(targetX - implicitWidth / 2)
|
|
||||||
top: Math.round(targetY)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: textContent
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.tooltipText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.SystemTray
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var parentWindow: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + horizontalPadding * 2 : 0
|
|
||||||
|
|
||||||
width: calculatedWidth
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SystemTray.items.values.length === 0) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
visible: SystemTray.items.values.length > 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: systemTrayRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: SystemTray.items.values
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
property var trayItem: modelData
|
|
||||||
property string iconSource: {
|
|
||||||
let icon = trayItem && trayItem.icon;
|
|
||||||
if (typeof icon === 'string' || icon instanceof String) {
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const split = icon.split("?path=");
|
|
||||||
if (split.length !== 2) {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = split[0];
|
|
||||||
const path = split[1];
|
|
||||||
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
source: parent.iconSource
|
|
||||||
asynchronous: true
|
|
||||||
smooth: true
|
|
||||||
mipmap: true
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: trayItemArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
if (!trayItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
|
|
||||||
trayItem.activate();
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (trayItem.hasMenu) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
menuAnchor.menu = trayItem.menu;
|
|
||||||
menuAnchor.anchor.window = parentWindow;
|
|
||||||
menuAnchor.anchor.rect = Qt.rect(relativeX, parentWindow.effectiveBarHeight + SettingsData.topBarSpacing, parent.width, 1);
|
|
||||||
menuAnchor.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QsMenuAnchor {
|
|
||||||
id: menuAnchor
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
|
|
||||||
readonly property bool isChecking: SystemUpdateService.isChecking
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
width: updaterIcon.width + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = updaterArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: updaterIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: statusIcon
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
name: {
|
|
||||||
if (isChecking) return "refresh";
|
|
||||||
if (SystemUpdateService.hasError) return "error";
|
|
||||||
if (hasUpdates) return "system_update_alt";
|
|
||||||
return "check_circle";
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: {
|
|
||||||
if (SystemUpdateService.hasError) return Theme.error;
|
|
||||||
if (hasUpdates) return Theme.primary;
|
|
||||||
return (updaterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText);
|
|
||||||
}
|
|
||||||
|
|
||||||
RotationAnimation {
|
|
||||||
id: rotationAnimation
|
|
||||||
target: statusIcon
|
|
||||||
property: "rotation"
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
running: isChecking
|
|
||||||
loops: Animation.Infinite
|
|
||||||
|
|
||||||
onRunningChanged: {
|
|
||||||
if (!running) {
|
|
||||||
statusIcon.rotation = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: countText
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: SystemUpdateService.updateCount.toString()
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
visible: hasUpdates && !isChecking
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: updaterArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
root.clicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,113 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// Passed in by TopBar
|
|
||||||
property int widgetHeight: 28
|
|
||||||
property int barHeight: 32
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal toggleVpnPopup()
|
|
||||||
|
|
||||||
width: Theme.iconSize + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = clickArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: icon
|
|
||||||
|
|
||||||
name: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off")
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: VpnService.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
|
||||||
running: VpnService.isBusy
|
|
||||||
loops: Animation.Infinite
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 900
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clickArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
root.toggleVpnPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: tooltip
|
|
||||||
|
|
||||||
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.widgetBaseBackgroundColor
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
visible: clickArea.containsMouse && !(popupTarget && popupTarget.shouldBeVisible)
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
opacity: clickArea.containsMouse ? 1 : 0
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: tooltipText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
if (!VpnService.connected) {
|
|
||||||
return "VPN Disconnected";
|
|
||||||
}
|
|
||||||
|
|
||||||
const names = VpnService.activeNames || [];
|
|
||||||
if (names.length <= 1) {
|
|
||||||
return "VPN Connected • " + (names[0] || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "VPN Connected • " + names[0] + " +" + (names.length - 1);
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string section: "center"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
visible: SettingsData.weatherEnabled
|
|
||||||
width: visible ? Math.min(100, weatherRow.implicitWidth + horizontalPadding * 2) : 0
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = weatherArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: WeatherService
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: weatherRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
|
|
||||||
if (temp === undefined || temp === null || temp === 0) {
|
|
||||||
return "--°" + (SettingsData.useFahrenheit ? "F" : "C");
|
|
||||||
}
|
|
||||||
|
|
||||||
return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C");
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: weatherArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0);
|
|
||||||
const currentScreen = parentScreen || Screen;
|
|
||||||
const screenX = currentScreen.x || 0;
|
|
||||||
const relativeX = globalPos.x - screenX;
|
|
||||||
popupTarget.setTriggerPosition(relativeX, barHeight + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance, width, section, currentScreen);
|
|
||||||
}
|
|
||||||
root.clicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,525 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string screenName: ""
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property int currentWorkspace: {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
return getNiriActiveWorkspace()
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
return getHyprlandActiveWorkspace()
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
property var workspaceList: {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
const baseList = getNiriWorkspaces()
|
|
||||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
|
||||||
}
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
const baseList = getHyprlandWorkspaces()
|
|
||||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
|
||||||
}
|
|
||||||
return [1]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkspaceIcons(ws) {
|
|
||||||
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 {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels
|
|
||||||
|
|
||||||
|
|
||||||
const byApp = {}
|
|
||||||
const isActiveWs = CompositorService.isNiri ? NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active) : targetWorkspaceId === root.currentWorkspace
|
|
||||||
|
|
||||||
wins.forEach((w, i) => {
|
|
||||||
if (!w) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let winWs = null
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
winWs = w.workspace_id
|
|
||||||
} else {
|
|
||||||
// For Hyprland, we need to find the corresponding Hyprland toplevel to get workspace
|
|
||||||
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").toLowerCase()
|
|
||||||
const key = isActiveWs ? `${keyBase}_${i}` : keyBase
|
|
||||||
|
|
||||||
if (!byApp[key]) {
|
|
||||||
const moddedId = Paths.moddedAppId(keyBase)
|
|
||||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
|
||||||
const icon = isSteamApp ? "" : Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
|
|
||||||
byApp[key] = {
|
|
||||||
"type": "icon",
|
|
||||||
"icon": icon,
|
|
||||||
"isSteamApp": isSteamApp,
|
|
||||||
"active": !!(w.activated || (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 || (CompositorService.isNiri && w.is_focused)) {
|
|
||||||
byApp[key].active = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Object.values(byApp)
|
|
||||||
}
|
|
||||||
|
|
||||||
function padWorkspaces(list) {
|
|
||||||
const padded = list.slice()
|
|
||||||
const placeholder = CompositorService.isHyprland ? {
|
|
||||||
"id": -1,
|
|
||||||
"name": ""
|
|
||||||
} : -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 getHyprlandWorkspaces() {
|
|
||||||
const workspaces = Hyprland.workspaces?.values || []
|
|
||||||
|
|
||||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
|
||||||
// Show all workspaces on all monitors if per-monitor filtering is disabled
|
|
||||||
const sorted = workspaces.slice().sort((a, b) => a.id - b.id)
|
|
||||||
return sorted.length > 0 ? sorted : [{
|
|
||||||
"id": 1,
|
|
||||||
"name": "1"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter workspaces for this specific monitor using lastIpcObject.monitor
|
|
||||||
// This matches the approach from the original kyle-config
|
|
||||||
const monitorWorkspaces = workspaces.filter(ws => {
|
|
||||||
return ws.lastIpcObject && ws.lastIpcObject.monitor === root.screenName
|
|
||||||
})
|
|
||||||
|
|
||||||
if (monitorWorkspaces.length === 0) {
|
|
||||||
// Fallback if no workspaces exist for this monitor
|
|
||||||
return [{
|
|
||||||
"id": 1,
|
|
||||||
"name": "1"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all workspaces for this monitor, sorted by ID
|
|
||||||
return monitorWorkspaces.sort((a, b) => a.id - b.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHyprlandActiveWorkspace() {
|
|
||||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
|
||||||
return Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the monitor object for this screen
|
|
||||||
const monitors = Hyprland.monitors?.values || []
|
|
||||||
const currentMonitor = monitors.find(monitor => monitor.name === root.screenName)
|
|
||||||
|
|
||||||
if (!currentMonitor) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the monitor's active workspace ID (like original config)
|
|
||||||
return currentMonitor.activeWorkspace?.id ?? 1
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real padding: (widgetHeight - workspaceRow.implicitHeight) / 2
|
|
||||||
|
|
||||||
function getRealWorkspaces() {
|
|
||||||
return root.workspaceList.filter(ws => {
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return ws && ws.id !== -1
|
|
||||||
}
|
|
||||||
return ws !== -1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchWorkspace(direction) {
|
|
||||||
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 ? (validIndex + 1) % realWorkspaces.length : (validIndex - 1 + realWorkspaces.length) % realWorkspaces.length
|
|
||||||
|
|
||||||
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1)
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
const command = direction > 0 ? "workspace r+1" : "workspace r-1"
|
|
||||||
Hyprland.dispatch(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: workspaceRow.implicitWidth + padding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground)
|
|
||||||
return "transparent"
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
|
||||||
|
|
||||||
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 direction = deltaY < 0 ? 1 : -1
|
|
||||||
|
|
||||||
if (isMouseWheel) {
|
|
||||||
switchWorkspace(direction)
|
|
||||||
} else {
|
|
||||||
scrollAccumulator += deltaY
|
|
||||||
|
|
||||||
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
|
|
||||||
const touchDirection = scrollAccumulator < 0 ? 1 : -1
|
|
||||||
switchWorkspace(touchDirection)
|
|
||||||
scrollAccumulator = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wheel.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: workspaceRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.workspaceList
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: delegateRoot
|
|
||||||
|
|
||||||
property bool isActive: {
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return modelData && modelData.id === root.currentWorkspace
|
|
||||||
}
|
|
||||||
return modelData === root.currentWorkspace
|
|
||||||
}
|
|
||||||
property bool isPlaceholder: {
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return modelData && modelData.id === -1
|
|
||||||
}
|
|
||||||
return modelData === -1
|
|
||||||
}
|
|
||||||
property bool isHovered: mouseArea.containsMouse
|
|
||||||
|
|
||||||
property var loadedWorkspaceData: null
|
|
||||||
property var loadedIconData: null
|
|
||||||
property bool loadedHasIcon: false
|
|
||||||
property var loadedIcons: []
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: dataUpdateTimer
|
|
||||||
interval: 50 // Defer data calculation by 50ms
|
|
||||||
onTriggered: {
|
|
||||||
if (isPlaceholder) {
|
|
||||||
delegateRoot.loadedWorkspaceData = null
|
|
||||||
delegateRoot.loadedIconData = null
|
|
||||||
delegateRoot.loadedHasIcon = false
|
|
||||||
delegateRoot.loadedIcons = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var wsData = null;
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null;
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
wsData = modelData;
|
|
||||||
}
|
|
||||||
delegateRoot.loadedWorkspaceData = wsData;
|
|
||||||
|
|
||||||
var icData = null;
|
|
||||||
if (wsData?.name) {
|
|
||||||
icData = SettingsData.getWorkspaceNameIcon(wsData.name);
|
|
||||||
}
|
|
||||||
delegateRoot.loadedIconData = icData;
|
|
||||||
delegateRoot.loadedHasIcon = icData !== null;
|
|
||||||
|
|
||||||
if (SettingsData.showWorkspaceApps) {
|
|
||||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
|
|
||||||
} else {
|
|
||||||
delegateRoot.loadedIcons = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAllData() {
|
|
||||||
dataUpdateTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
width: {
|
|
||||||
if (SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
|
||||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons);
|
|
||||||
const iconsWidth = numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0);
|
|
||||||
const baseWidth = isActive ? root.widgetHeight * 1.0 + Theme.spacingXS : root.widgetHeight * 0.8;
|
|
||||||
return baseWidth + iconsWidth;
|
|
||||||
}
|
|
||||||
return isActive ? root.widgetHeight * 1.2 : root.widgetHeight * 0.8;
|
|
||||||
}
|
|
||||||
height: SettingsData.showWorkspaceApps ? widgetHeight * 0.8 : widgetHeight * 0.6
|
|
||||||
radius: height / 2
|
|
||||||
color: isActive ? Theme.primary : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
enabled: (!SettingsData.showWorkspaceApps || SettingsData.maxWorkspaceIcons <= 3)
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: !isPlaceholder
|
|
||||||
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
|
||||||
enabled: !isPlaceholder
|
|
||||||
onClicked: {
|
|
||||||
if (isPlaceholder) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
NiriService.switchToWorkspace(modelData - 1)
|
|
||||||
} else if (CompositorService.isHyprland && modelData?.id) {
|
|
||||||
Hyprland.dispatch(`workspace ${modelData.id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loader for App Icons
|
|
||||||
Loader {
|
|
||||||
id: appIconsLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 4
|
|
||||||
visible: loadedIcons.length > 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: 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
|
|
||||||
hoverEnabled: true
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: isActive
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
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.fontSizeSmall
|
|
||||||
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: {
|
|
||||||
const isPlaceholder = CompositorService.isHyprland ? (modelData?.id === -1) : (modelData === -1)
|
|
||||||
if (isPlaceholder) {
|
|
||||||
return index + 1
|
|
||||||
}
|
|
||||||
return CompositorService.isHyprland ? (modelData?.id || "") : (modelData - 1);
|
|
||||||
}
|
|
||||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- LOGIC / TRIGGERS ---
|
|
||||||
Component.onCompleted: updateAllData()
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: CompositorService
|
|
||||||
function onSortedToplevelsChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: NiriService
|
|
||||||
enabled: CompositorService.isNiri
|
|
||||||
function onAllWorkspacesChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() }
|
|
||||||
function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
active: true
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("wallpaper")
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: wallpaperWindow
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
screen: modelData
|
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayer.Background
|
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
||||||
|
|
||||||
anchors.top: true
|
|
||||||
anchors.bottom: true
|
|
||||||
anchors.left: true
|
|
||||||
anchors.right: true
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
property string source: SessionData.getMonitorWallpaper(modelData.name) || ""
|
|
||||||
property bool isColorSource: source.startsWith("#")
|
|
||||||
property string transitionType: SessionData.wallpaperTransition
|
|
||||||
property string actualTransitionType: transitionType
|
|
||||||
onTransitionTypeChanged: {
|
|
||||||
if (transitionType === "random") {
|
|
||||||
if (SessionData.includedTransitions.length === 0) {
|
|
||||||
actualTransitionType = "none"
|
|
||||||
} else {
|
|
||||||
actualTransitionType = SessionData.includedTransitions[Math.floor(Math.random() * SessionData.includedTransitions.length)]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
actualTransitionType = transitionType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onActualTransitionTypeChanged: {
|
|
||||||
if (actualTransitionType === "none") {
|
|
||||||
currentWallpaper.visible = true
|
|
||||||
nextWallpaper.visible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
property real transitionProgress: 0
|
|
||||||
property real fillMode: 1.0
|
|
||||||
property vector4d fillColor: Qt.vector4d(0, 0, 0, 1)
|
|
||||||
property real edgeSmoothness: 0.1
|
|
||||||
|
|
||||||
property real wipeDirection: 0
|
|
||||||
property real discCenterX: 0.5
|
|
||||||
property real discCenterY: 0.5
|
|
||||||
property real stripesCount: 16
|
|
||||||
property real stripesAngle: 0
|
|
||||||
|
|
||||||
readonly property bool transitioning: transitionAnimation.running
|
|
||||||
|
|
||||||
property bool hasCurrent: currentWallpaper.status === Image.Ready && !!currentWallpaper.source
|
|
||||||
property bool booting: !hasCurrent && nextWallpaper.status === Image.Ready
|
|
||||||
|
|
||||||
WallpaperEngineProc {
|
|
||||||
id: weProc
|
|
||||||
monitor: modelData.name
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
weProc.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
onSourceChanged: {
|
|
||||||
const isWE = source.startsWith("we:")
|
|
||||||
const isColor = source.startsWith("#")
|
|
||||||
|
|
||||||
if (isWE) {
|
|
||||||
setWallpaperImmediate("")
|
|
||||||
weProc.start(source.substring(3))
|
|
||||||
} else {
|
|
||||||
weProc.stop()
|
|
||||||
if (!source) {
|
|
||||||
setWallpaperImmediate("")
|
|
||||||
} else if (isColor) {
|
|
||||||
setWallpaperImmediate("")
|
|
||||||
} else {
|
|
||||||
// Always set immediately if there's no current wallpaper (startup)
|
|
||||||
if (!currentWallpaper.source) {
|
|
||||||
setWallpaperImmediate(source.startsWith("file://") ? source : "file://" + source)
|
|
||||||
} else {
|
|
||||||
changeWallpaper(source.startsWith("file://") ? source : "file://" + source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperImmediate(newSource) {
|
|
||||||
transitionAnimation.stop()
|
|
||||||
root.transitionProgress = 0.0
|
|
||||||
currentWallpaper.source = newSource
|
|
||||||
nextWallpaper.source = ""
|
|
||||||
currentWallpaper.visible = true
|
|
||||||
nextWallpaper.visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeWallpaper(newPath, force) {
|
|
||||||
if (!force && newPath === currentWallpaper.source) return
|
|
||||||
if (!newPath || newPath.startsWith("#")) return
|
|
||||||
|
|
||||||
if (root.transitioning) {
|
|
||||||
transitionAnimation.stop()
|
|
||||||
root.transitionProgress = 0
|
|
||||||
currentWallpaper.source = nextWallpaper.source
|
|
||||||
nextWallpaper.source = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no current wallpaper, set immediately to avoid scaling issues
|
|
||||||
if (!currentWallpaper.source) {
|
|
||||||
setWallpaperImmediate(newPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If transition is "none", set immediately
|
|
||||||
if (root.transitionType === "random") {
|
|
||||||
if (SessionData.includedTransitions.length === 0) {
|
|
||||||
root.actualTransitionType = "none"
|
|
||||||
} else {
|
|
||||||
root.actualTransitionType = SessionData.includedTransitions[Math.floor(Math.random() * SessionData.includedTransitions.length)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.actualTransitionType === "none") {
|
|
||||||
setWallpaperImmediate(newPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.actualTransitionType === "wipe") {
|
|
||||||
root.wipeDirection = Math.random() * 4
|
|
||||||
} else if (root.actualTransitionType === "disc") {
|
|
||||||
root.discCenterX = Math.random()
|
|
||||||
root.discCenterY = Math.random()
|
|
||||||
} else if (root.actualTransitionType === "stripes") {
|
|
||||||
root.stripesCount = Math.round(Math.random() * 20 + 4)
|
|
||||||
root.stripesAngle = Math.random() * 360
|
|
||||||
}
|
|
||||||
|
|
||||||
nextWallpaper.source = newPath
|
|
||||||
|
|
||||||
if (nextWallpaper.status === Image.Ready) {
|
|
||||||
transitionAnimation.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
active: !root.source || root.isColorSource
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: DankBackdrop {
|
|
||||||
screenName: modelData.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: transparentRect
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderEffectSource {
|
|
||||||
id: transparentSource
|
|
||||||
sourceItem: transparentRect
|
|
||||||
hideSource: true
|
|
||||||
live: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: currentWallpaper
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.actualTransitionType === "none"
|
|
||||||
opacity: 1
|
|
||||||
layer.enabled: false
|
|
||||||
asynchronous: true
|
|
||||||
smooth: true
|
|
||||||
cache: true
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: nextWallpaper
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: false
|
|
||||||
opacity: 0
|
|
||||||
layer.enabled: false
|
|
||||||
asynchronous: true
|
|
||||||
smooth: true
|
|
||||||
cache: true
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
|
|
||||||
onStatusChanged: {
|
|
||||||
if (status !== Image.Ready) return
|
|
||||||
|
|
||||||
if (root.actualTransitionType === "none") {
|
|
||||||
currentWallpaper.source = source
|
|
||||||
nextWallpaper.source = ""
|
|
||||||
root.transitionProgress = 0.0
|
|
||||||
} else {
|
|
||||||
currentWallpaper.layer.enabled = true
|
|
||||||
layer.enabled = true
|
|
||||||
visible = true
|
|
||||||
if (!root.transitioning) {
|
|
||||||
transitionAnimation.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderEffect {
|
|
||||||
id: fadeShader
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.actualTransitionType === "fade" && (root.hasCurrent || root.booting)
|
|
||||||
|
|
||||||
property variant source1: root.hasCurrent ? currentWallpaper : transparentSource
|
|
||||||
property variant source2: nextWallpaper
|
|
||||||
property real progress: root.transitionProgress
|
|
||||||
property real fillMode: root.fillMode
|
|
||||||
property vector4d fillColor: root.fillColor
|
|
||||||
property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : modelData.width)
|
|
||||||
property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : modelData.height)
|
|
||||||
property real imageWidth2: Math.max(1, source2.sourceSize.width)
|
|
||||||
property real imageHeight2: Math.max(1, source2.sourceSize.height)
|
|
||||||
property real screenWidth: modelData.width
|
|
||||||
property real screenHeight: modelData.height
|
|
||||||
|
|
||||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_fade.frag.qsb")
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderEffect {
|
|
||||||
id: wipeShader
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.actualTransitionType === "wipe" && (root.hasCurrent || root.booting)
|
|
||||||
|
|
||||||
property variant source1: root.hasCurrent ? currentWallpaper : transparentSource
|
|
||||||
property variant source2: nextWallpaper
|
|
||||||
property real progress: root.transitionProgress
|
|
||||||
property real smoothness: root.edgeSmoothness
|
|
||||||
property real direction: root.wipeDirection
|
|
||||||
property real fillMode: root.fillMode
|
|
||||||
property vector4d fillColor: root.fillColor
|
|
||||||
property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : modelData.width)
|
|
||||||
property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : modelData.height)
|
|
||||||
property real imageWidth2: Math.max(1, source2.sourceSize.width)
|
|
||||||
property real imageHeight2: Math.max(1, source2.sourceSize.height)
|
|
||||||
property real screenWidth: modelData.width
|
|
||||||
property real screenHeight: modelData.height
|
|
||||||
|
|
||||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_wipe.frag.qsb")
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderEffect {
|
|
||||||
id: discShader
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.actualTransitionType === "disc" && (root.hasCurrent || root.booting)
|
|
||||||
|
|
||||||
property variant source1: root.hasCurrent ? currentWallpaper : transparentSource
|
|
||||||
property variant source2: nextWallpaper
|
|
||||||
property real progress: root.transitionProgress
|
|
||||||
property real smoothness: root.edgeSmoothness
|
|
||||||
property real aspectRatio: root.width / root.height
|
|
||||||
property real centerX: root.discCenterX
|
|
||||||
property real centerY: root.discCenterY
|
|
||||||
property real fillMode: root.fillMode
|
|
||||||
property vector4d fillColor: root.fillColor
|
|
||||||
property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : modelData.width)
|
|
||||||
property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : modelData.height)
|
|
||||||
property real imageWidth2: Math.max(1, source2.sourceSize.width)
|
|
||||||
property real imageHeight2: Math.max(1, source2.sourceSize.height)
|
|
||||||
property real screenWidth: modelData.width
|
|
||||||
property real screenHeight: modelData.height
|
|
||||||
|
|
||||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_disc.frag.qsb")
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderEffect {
|
|
||||||
id: stripesShader
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.actualTransitionType === "stripes" && (root.hasCurrent || root.booting)
|
|
||||||
|
|
||||||
property variant source1: root.hasCurrent ? currentWallpaper : transparentSource
|
|
||||||
property variant source2: nextWallpaper
|
|
||||||
property real progress: root.transitionProgress
|
|
||||||
property real smoothness: root.edgeSmoothness
|
|
||||||
property real aspectRatio: root.width / root.height
|
|
||||||
property real stripeCount: root.stripesCount
|
|
||||||
property real angle: root.stripesAngle
|
|
||||||
property real fillMode: root.fillMode
|
|
||||||
property vector4d fillColor: root.fillColor
|
|
||||||
property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : modelData.width)
|
|
||||||
property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : modelData.height)
|
|
||||||
property real imageWidth2: Math.max(1, source2.sourceSize.width)
|
|
||||||
property real imageHeight2: Math.max(1, source2.sourceSize.height)
|
|
||||||
property real screenWidth: modelData.width
|
|
||||||
property real screenHeight: modelData.height
|
|
||||||
|
|
||||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_stripes.frag.qsb")
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderEffect {
|
|
||||||
id: irisBloomShader
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.actualTransitionType === "iris bloom" && (root.hasCurrent || root.booting)
|
|
||||||
|
|
||||||
property variant source1: root.hasCurrent ? currentWallpaper : transparentSource
|
|
||||||
property variant source2: nextWallpaper
|
|
||||||
property real progress: root.transitionProgress
|
|
||||||
property real smoothness: root.edgeSmoothness
|
|
||||||
property real centerX: 0.5
|
|
||||||
property real centerY: 0.5
|
|
||||||
property real aspectRatio: root.width / root.height
|
|
||||||
property real fillMode: root.fillMode
|
|
||||||
property vector4d fillColor: root.fillColor
|
|
||||||
property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : modelData.width)
|
|
||||||
property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : modelData.height)
|
|
||||||
property real imageWidth2: Math.max(1, source2.sourceSize.width)
|
|
||||||
property real imageHeight2: Math.max(1, source2.sourceSize.height)
|
|
||||||
property real screenWidth: modelData.width
|
|
||||||
property real screenHeight: modelData.height
|
|
||||||
|
|
||||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_iris_bloom.frag.qsb")
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderEffect {
|
|
||||||
id: pixelateShader
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.actualTransitionType === "pixelate" && (root.hasCurrent || root.booting)
|
|
||||||
|
|
||||||
property variant source1: root.hasCurrent ? currentWallpaper : transparentSource
|
|
||||||
property variant source2: nextWallpaper
|
|
||||||
property real progress: root.transitionProgress
|
|
||||||
property real smoothness: root.edgeSmoothness // controls starting block size
|
|
||||||
property real fillMode: root.fillMode
|
|
||||||
property vector4d fillColor: root.fillColor
|
|
||||||
property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : modelData.width)
|
|
||||||
property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : modelData.height)
|
|
||||||
property real imageWidth2: Math.max(1, source2.sourceSize.width)
|
|
||||||
property real imageHeight2: Math.max(1, source2.sourceSize.height)
|
|
||||||
property real screenWidth: modelData.width
|
|
||||||
property real screenHeight: modelData.height
|
|
||||||
property real centerX: root.discCenterX
|
|
||||||
property real centerY: root.discCenterY
|
|
||||||
property real aspectRatio: root.width / root.height
|
|
||||||
|
|
||||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_pixelate.frag.qsb")
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderEffect {
|
|
||||||
id: portalShader
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.actualTransitionType === "portal" && (root.hasCurrent || root.booting)
|
|
||||||
|
|
||||||
property variant source1: root.hasCurrent ? currentWallpaper : transparentSource
|
|
||||||
property variant source2: nextWallpaper
|
|
||||||
property real progress: root.transitionProgress
|
|
||||||
property real smoothness: root.edgeSmoothness
|
|
||||||
property real aspectRatio: root.width / root.height
|
|
||||||
property real centerX: root.discCenterX
|
|
||||||
property real centerY: root.discCenterY
|
|
||||||
property real fillMode: root.fillMode
|
|
||||||
property vector4d fillColor: root.fillColor
|
|
||||||
property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : modelData.width)
|
|
||||||
property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : modelData.height)
|
|
||||||
property real imageWidth2: Math.max(1, source2.sourceSize.width)
|
|
||||||
property real imageHeight2: Math.max(1, source2.sourceSize.height)
|
|
||||||
property real screenWidth: modelData.width
|
|
||||||
property real screenHeight: modelData.height
|
|
||||||
|
|
||||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_portal.frag.qsb")
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: transitionAnimation
|
|
||||||
target: root
|
|
||||||
property: "transitionProgress"
|
|
||||||
from: 0.0
|
|
||||||
to: 1.0
|
|
||||||
duration: root.actualTransitionType === "none" ? 0 : 1000
|
|
||||||
easing.type: Easing.InOutCubic
|
|
||||||
onFinished: {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (nextWallpaper.source && nextWallpaper.status === Image.Ready && !nextWallpaper.source.toString().startsWith("#")) {
|
|
||||||
currentWallpaper.source = nextWallpaper.source
|
|
||||||
}
|
|
||||||
nextWallpaper.source = ""
|
|
||||||
nextWallpaper.visible = false
|
|
||||||
currentWallpaper.visible = root.actualTransitionType === "none"
|
|
||||||
currentWallpaper.layer.enabled = false
|
|
||||||
nextWallpaper.layer.enabled = false
|
|
||||||
root.transitionProgress = 0.0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property string monitor: ""
|
|
||||||
property string sceneId: ""
|
|
||||||
property string pendingSceneId: ""
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: weProcess
|
|
||||||
running: false
|
|
||||||
command: []
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: killer
|
|
||||||
running: false
|
|
||||||
command: []
|
|
||||||
onExited: (code) => {
|
|
||||||
if (pendingSceneId !== "") {
|
|
||||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
|
|
||||||
const baseDir = Paths.strip(cacheHome)
|
|
||||||
const outDir = baseDir + "/dankshell/we_screenshots"
|
|
||||||
const outPath = outDir + "/" + pendingSceneId + ".jpg"
|
|
||||||
|
|
||||||
Quickshell.execDetached(["mkdir", "-p", outDir])
|
|
||||||
weProcess.command = [
|
|
||||||
"linux-wallpaperengine",
|
|
||||||
"--screen-root", monitor,
|
|
||||||
"--screenshot", outPath,
|
|
||||||
"--bg", pendingSceneId,
|
|
||||||
"--silent"
|
|
||||||
]
|
|
||||||
weProcess.running = true
|
|
||||||
sceneId = pendingSceneId
|
|
||||||
pendingSceneId = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function start(newSceneId) {
|
|
||||||
if (sceneId === newSceneId && weProcess.running) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pendingSceneId = newSceneId
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop() {
|
|
||||||
if (weProcess.running) {
|
|
||||||
weProcess.running = false
|
|
||||||
}
|
|
||||||
killer.command = [
|
|
||||||
"pkill", "-f",
|
|
||||||
"linux-wallpaperengine --screen-root " + monitor
|
|
||||||
]
|
|
||||||
killer.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
710
README.md
710
README.md
@@ -1,677 +1,187 @@
|
|||||||
# DankMaterialShell (dms)
|
# DankMaterialShell
|
||||||
|
|
||||||
<div align=center>
|
<div align="center">
|
||||||
|
<a href="https://danklinux.com">
|
||||||
|
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### A modern desktop shell for Wayland
|
||||||
|
|
||||||
|
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/stargazers)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/commits/master)
|
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||||
[](https://aur.archlinux.org/packages/dms-shell)
|
|
||||||
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||||
|
[](https://ko-fi.com/avengemediallc)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and designed for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors. Features Material 3 design principles with a heavy focus on functionality and customizability.
|
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), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
||||||
|
|
||||||
## Screenshots
|
## Repository Structure
|
||||||
|
|
||||||
<div align="center">
|
This is a monorepo containing both the shell interface and backend services:
|
||||||
<div style="max-width: 700px; margin: 0 auto;">
|
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/fd619c0e-6edc-457e-b3d6-5a5c3bae7173
|
```
|
||||||
|
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
|
||||||
|
├── backend/ # Go backend and CLI
|
||||||
|
│ ├── cmd/ # dms CLI and dankinstall binaries
|
||||||
|
│ ├── internal/ # System integration, IPC, distro support
|
||||||
|
│ └── pkg/ # Shared packages
|
||||||
|
├── distro/ # Distribution packaging (Fedora RPM specs)
|
||||||
|
├── nix/ # NixOS/home-manager modules
|
||||||
|
└── flake.nix # Nix flake for declarative installation
|
||||||
|
```
|
||||||
|
|
||||||
</div>
|
## See it in Action
|
||||||
</div>
|
|
||||||
|
|
||||||
<details><summary><strong>View More Screenshots</strong></summary>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
### Desktop Overview
|
https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/203a9678-c3b7-4720-bb97-853a511ac5c8" width="600" alt="DankMaterialShell Desktop" />
|
</div>
|
||||||
|
|
||||||
### Dashboard
|
<details><summary><strong>More Screenshots</strong></summary>
|
||||||
|
|
||||||
<img width="600" alt="DankDash" src="https://github.com/user-attachments/assets/a937cf35-a43b-4558-8c39-5694ff5fcac4" />
|
<div align="center">
|
||||||
|
|
||||||
### Application Launcher
|
<img src="https://github.com/user-attachments/assets/203a9678-c3b7-4720-bb97-853a511ac5c8" width="600" alt="Desktop" />
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/2da00ea1-8921-4473-a2a9-44a44535a822" width="450" alt="Spotlight Launcher" />
|
<img src="https://github.com/user-attachments/assets/a937cf35-a43b-4558-8c39-5694ff5fcac4" width="600" alt="Dashboard" />
|
||||||
|
|
||||||
### Control Center
|
<img src="https://github.com/user-attachments/assets/2da00ea1-8921-4473-a2a9-44a44535a822" width="450" alt="Launcher" />
|
||||||
|
|
||||||
<img width="600" alt="Control Center" src="https://github.com/user-attachments/assets/98889bd8-55d2-44c7-b278-75ca49c596fa" />
|
<img src="https://github.com/user-attachments/assets/732c30de-5f4a-4a2b-a995-c8ab656cecd5" width="600" alt="Control Center" />
|
||||||
|
|
||||||
### System Monitor
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/b3c817ec-734d-4974-929f-2d11a1065349" width="600" alt="System Monitor" />
|
|
||||||
|
|
||||||
### Widget Customization
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/903f7c60-146f-4fb3-a75d-a4823828f298" width="500" alt="Widget Customization" />
|
|
||||||
|
|
||||||
### Lock Screen
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/3fa07de2-c1b0-4e57-8f25-3830ac6baf4f" width="600" alt="Lock Screen" />
|
|
||||||
|
|
||||||
### Dynamic Theming
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/a81a68e3-4f7e-4246-8199-0fef1013d4cf" width="700" alt="Auto Theme" />
|
|
||||||
|
|
||||||
### Notification Center
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/07cbde9a-0242-4989-9f97-5765c6458c85" width="350" alt="Notification Center" />
|
|
||||||
|
|
||||||
### Dock
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/e6999daf-f7bf-4329-98fa-0ce4f0e7219c" width="400" alt="Dock" />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Quick start (full dotfiles, most distros)
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://install.danklinux.com | sh
|
curl -fsSL https://install.danklinux.com | sh
|
||||||
```
|
```
|
||||||
*Or skip to [Installation](https://github.com/AvengeMedia/DankMaterialShell?tab=readme-ov-file#installation)*
|
|
||||||
|
|
||||||
<details><summary><strong>Features</strong></summary>
|
One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
|
||||||
|
|
||||||
**Core Widgets:**
|
**[Manual installation guide](https://danklinux.com/docs/dankmaterialshell/installation)**
|
||||||
- **TopBar**: fully customizable bar where widgets can be added, removed, and re-arranged.
|
|
||||||
- **App Launcher** with fuzzy search, categories, and auto-sorting by most used apps.
|
|
||||||
- **Workspace Switcher** Configurable workspace switcher.
|
|
||||||
- **Focused Window** Displays the currently focused window app name and title.
|
|
||||||
- **Running Apps** A view of all running apps, sorted by monitor, workspace, then position on workspace.
|
|
||||||
- **Media Player** Short form media player with equalizer, song title, and controls.
|
|
||||||
- **Clock** Clock and date widget
|
|
||||||
- **Weather** Weather widget with customizable location
|
|
||||||
- **System Tray** System tray applets with context menus.
|
|
||||||
- **Process Monitor** CPU, RAM, and GPU usage percentages, temperatures. (requires [dgop](https://github.com/AvengeMedia/dgop))
|
|
||||||
- **Power/Battery** Power/Battery widget for battery metrics and power profile changing.
|
|
||||||
- **Notifications** Notification bell with a notification center popup
|
|
||||||
- **Control Center** High-level view of network, bluetooth, and audio status
|
|
||||||
- **Privacy Indicator** Attempts to reveal if a microphone or screen recording session is active, relying on Pipewire data sources
|
|
||||||
- **Idle Inhibitor** Creates a systemd idle inhibitor to prevent sleep/locking from occuring.
|
|
||||||
- **Spotlight Launcher** A central app launcher/search that can be triggered via an IPC keybinding.
|
|
||||||
- **Central Command** A combined music, weather, calendar, and events PopUp.
|
|
||||||
- **Process List** A process list, with system metrics and information. More detailed modal available via IPC.
|
|
||||||
- **Notification Center** A center for notifications that has support for grouping.
|
|
||||||
- **Dock** A dock with pinned apps support, recent apps support, and currently running application support.
|
|
||||||
- **Control Center** A full control center with user profile information, network, bluetooth, audio input/output, display controls, and night mode automation.
|
|
||||||
- **Lock Screen** Using quickshell's WlSessionLock with embedded virtual keyboard for Niri (Niri doesn't support placing virtual keyboard above lockscreen natively: [issue](https://github.com/YaLTeR/niri/issues/2201))
|
|
||||||
- **Notepad** A simple text notepad/scratchpad with auto-save to session data and file export/import functionality.
|
|
||||||
|
|
||||||
</details>
|
## Features
|
||||||
|
|
||||||
## Highlights
|
**Dynamic Theming**
|
||||||
|
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
|
||||||
|
|
||||||
- Auto-theming GTK, QT, Terminal apps, and more with [matugen](https://github.com/InioX/matugen) + optional theme generation from wallpaper.
|
**System Monitoring**
|
||||||
- 20+ widgets that can be added and re-arranged on the bar.
|
Real-time CPU, RAM, GPU metrics and temperatures with [dgop](https://github.com/AvengeMedia/dgop). Process list with search and management.
|
||||||
- Process list, temperature monitoring, and resource monitoring with [dgop](https://github.com/AvengeMedia/dgop)
|
|
||||||
- Notification service with support for grouping and richtext
|
|
||||||
- App launcher + Spotlighht launcher with fuzzy search
|
|
||||||
- Control center with mpris player, weather, and calendar integration.
|
|
||||||
- Clipboard history view with image previews.
|
|
||||||
- A dock for running apps + pinned apps
|
|
||||||
- Configure bluetooth, wifi, and audio input+output devices.
|
|
||||||
- A lock screen
|
|
||||||
- Idle monitoring - configure auto lock, screen off, suspend, and hibernate with different knobs for battery + AC power.
|
|
||||||
|
|
||||||
**TL;DR** *dms replaces your waybar, swaylock, swayidle, hypridle, hyprlock, fuzzels, walker, mako, and basically everything you use to stitch a desktop together*
|
**Powerful Launcher**
|
||||||
|
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
|
||||||
|
|
||||||
## Installation
|
**Control Center**
|
||||||
|
Unified interface for network, Bluetooth, audio devices, display settings, and night mode.
|
||||||
|
|
||||||
### Compositor Setup
|
**Smart Notifications**
|
||||||
|
Notification center with grouping, rich text support, and keyboard navigation.
|
||||||
|
|
||||||
DankMaterialShell supports both **niri** and **Hyprland** compositors:
|
**Media Integration**
|
||||||
|
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
||||||
|
|
||||||
**Niri**:
|
**Session Management**
|
||||||
```bash
|
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
||||||
# Arch Linux
|
|
||||||
paru -S niri-git
|
|
||||||
|
|
||||||
# Fedora
|
**Plugin System**
|
||||||
sudo dnf copr enable yalter/niri && sudo dnf install niri
|
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
||||||
```
|
|
||||||
|
|
||||||
For detailed niri installation instructions, see the [niri Getting Started guide](https://yalter.github.io/niri/Getting-Started.html).
|
## Supported Compositors
|
||||||
|
|
||||||
**Hyprland**:
|
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), and [MangoWC](https://github.com/DreamMaoMao/mangowc) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
||||||
```bash
|
|
||||||
# Arch Linux
|
|
||||||
sudo pacman -S hyprland
|
|
||||||
|
|
||||||
# Or from AUR for latest
|
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
||||||
paru -S hyprland-git
|
|
||||||
|
|
||||||
# Fedora
|
## Command Line Interface
|
||||||
sudo dnf install hyprland
|
|
||||||
|
|
||||||
# Or use Copr for latest builds
|
Control the shell from the command line or keybinds:
|
||||||
sudo dnf copr enable solopasha/hyprland && sudo dnf install hyprland
|
|
||||||
```
|
|
||||||
|
|
||||||
For detailed Hyprland installation instructions, see the [Hyprland wiki](https://wiki.hypr.land/Getting-Started/Installation/).
|
|
||||||
|
|
||||||
### Dank Shell Installation
|
|
||||||
|
|
||||||
*feel free to contribute steps for other distributions*
|
|
||||||
|
|
||||||
#### Arch Linux - via AUR
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
paru -S dms-shell-git
|
dms run # Start the shell
|
||||||
```
|
|
||||||
|
|
||||||
#### nixOS - via flake
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix profile install github:AvengeMedia/DankMaterialShell
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Other Distributions - via manual installation
|
|
||||||
|
|
||||||
**1. Install Quickshell (Varies by Distribution)**
|
|
||||||
```bash
|
|
||||||
# Arch
|
|
||||||
paru -S quickshell-git
|
|
||||||
# Fedora
|
|
||||||
sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git
|
|
||||||
# ! TODO - document other distros
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Install fonts**
|
|
||||||
*Inter Variable* and *Fira Code* are not strictly required, but they are the default fonts of dms.
|
|
||||||
|
|
||||||
**2.1 Install Material Symbols**
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/.local/share/fonts &&
|
|
||||||
curl -L "https://github.com/google/material-design-icons/raw/master/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.ttf" -o ~/.local/share/fonts/MaterialSymbolsRounded.ttf
|
|
||||||
```
|
|
||||||
**2.2 Install Inter Variable**
|
|
||||||
```bash
|
|
||||||
curl -L "https://github.com/rsms/inter/raw/refs/tags/v4.1/docs/font-files/InterVariable.ttf" -o ~/.local/share/fonts/InterVariable.ttf
|
|
||||||
```
|
|
||||||
|
|
||||||
**2.3 Install Fira Code (monospace font)**
|
|
||||||
```bash
|
|
||||||
curl -L "https://github.com/tonsky/FiraCode/releases/latest/download/FiraCode-Regular.ttf" -o ~/.local/share/fonts/FiraCode-Regular.ttf
|
|
||||||
```
|
|
||||||
|
|
||||||
**2.4 Refresh font cache**
|
|
||||||
```bash
|
|
||||||
fc-cache -fv
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Install the shell**
|
|
||||||
|
|
||||||
**3.1. Clone latest master**
|
|
||||||
```bash
|
|
||||||
mkdir ~/.config/quickshell && git clone https://github.com/AvengeMedia/DankMaterialShell.git ~/.config/quickshell/dms
|
|
||||||
```
|
|
||||||
|
|
||||||
**3.2. Install latest dms CLI**
|
|
||||||
```bash
|
|
||||||
sudo sh -c "curl -L https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').gz | gunzip | tee /usr/local/bin/dms > /dev/null && chmod +x /usr/local/bin/dms"
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Optional Features (system monitoring, clipboard history, brightness controls, etc.)**
|
|
||||||
|
|
||||||
**4.1 Core optional dependencies**
|
|
||||||
```bash
|
|
||||||
# Arch Linux
|
|
||||||
sudo pacman -S cava wl-clipboard cliphist brightnessctl
|
|
||||||
paru -S matugen-bin dgop
|
|
||||||
|
|
||||||
# Fedora
|
|
||||||
sudo dnf install cava wl-clipboard brightnessctl
|
|
||||||
sudo dnf copr enable wef/cliphist && sudo dnf install cliphist
|
|
||||||
sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen
|
|
||||||
```
|
|
||||||
|
|
||||||
*Other distros will just need to find sources for the above packages*
|
|
||||||
|
|
||||||
**4.2 - dgop manual installation**
|
|
||||||
|
|
||||||
`dgop` is available via AUR and a nix flake, other distributions can install it manually.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo sh -c "curl -L https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').gz | gunzip | tee /usr/local/bin/dgop > /dev/null && chmod +x /usr/local/bin/dgop"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Optional Requirement Overview**
|
|
||||||
|
|
||||||
- `dgop`: Ability to have system resource widgets, process list modal, and temperature monitoring.
|
|
||||||
- `matugen`: Wallpaper-based dynamic theming
|
|
||||||
- `brightnessctl`: Backlight and LED brightness control
|
|
||||||
- `wl-clipboard`: Required for copying various elements to clipboard.
|
|
||||||
- `cava`: Audio visualizer
|
|
||||||
- `cliphist`: Clipboard history
|
|
||||||
- `gammastep`: Night mode support
|
|
||||||
|
|
||||||
## Compositor Configuration
|
|
||||||
|
|
||||||
A lot of options are subject to personal preference, but the below sets a good starting point for most features.
|
|
||||||
|
|
||||||
### Niri Integration
|
|
||||||
|
|
||||||
Add to your niri config
|
|
||||||
|
|
||||||
```kdl
|
|
||||||
// Required for clipboard history integration
|
|
||||||
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
|
||||||
|
|
||||||
// Recommended (must install polkit-mate before hand) for elevation prompts
|
|
||||||
spawn-at-startup "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
|
|
||||||
// This may be a different path on different distributions, the above is for the arch linux mate-polkit package
|
|
||||||
|
|
||||||
// Starts DankShell
|
|
||||||
spawn-at-startup "dms" "run"
|
|
||||||
|
|
||||||
// If using niri newer than 271534e115e5915231c99df287bbfe396185924d (~aug 17 2025)
|
|
||||||
// you can add this to disable built in config load errors since dank shell provides this
|
|
||||||
config-notification {
|
|
||||||
disable-failed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dank keybinds
|
|
||||||
// 1. These should not be in conflict with any pre-existing keybindings
|
|
||||||
// 2. You need to merge them with your existing config if you want to use these
|
|
||||||
// 3. You can change the keys to whatever you want, if you prefer something different
|
|
||||||
// 4. For the increment/decrement ones you can change the steps to whatever you like too
|
|
||||||
binds {
|
|
||||||
Mod+Space hotkey-overlay-title="Application Launcher" {
|
|
||||||
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
|
||||||
}
|
|
||||||
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
|
||||||
}
|
|
||||||
Mod+M hotkey-overlay-title="Task Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
|
||||||
}
|
|
||||||
Mod+N hotkey-overlay-title="Notification Center" {
|
|
||||||
spawn "dms" "ipc" "call" "notifications" "toggle";
|
|
||||||
}
|
|
||||||
Mod+Comma hotkey-overlay-title="Settings" {
|
|
||||||
spawn "dms" "ipc" "call" "settings" "toggle";
|
|
||||||
}
|
|
||||||
Mod+P hotkey-overlay-title="Notepad" {
|
|
||||||
spawn "dms" "ipc" "call" "notepad" "toggle";
|
|
||||||
}
|
|
||||||
Super+Alt+L hotkey-overlay-title="Lock Screen" {
|
|
||||||
spawn "dms" "ipc" "call" "lock" "lock";
|
|
||||||
}
|
|
||||||
Mod+X hotkey-overlay-title="Power Menu" {
|
|
||||||
spawn "dms" "ipc" "call" "powermenu" "toggle";
|
|
||||||
}
|
|
||||||
Mod+C hotkey-overlay-title="Control Center" {
|
|
||||||
spawn "dms" "ipc" "call" "control-center" "toggle";
|
|
||||||
}
|
|
||||||
XF86AudioRaiseVolume allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
|
||||||
}
|
|
||||||
XF86AudioLowerVolume allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
|
||||||
}
|
|
||||||
XF86AudioMute allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "mute";
|
|
||||||
}
|
|
||||||
XF86AudioMicMute allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
|
||||||
}
|
|
||||||
XF86MonBrightnessUp allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
|
|
||||||
}
|
|
||||||
// You can override the default device for e.g. keyboards by adding the device name to the last param
|
|
||||||
XF86MonBrightnessDown allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
|
|
||||||
}
|
|
||||||
// Night mode toggle
|
|
||||||
Mod+Shift+N allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "night" "toggle";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hyprland Integration
|
|
||||||
|
|
||||||
Add to your Hyprland config (`~/.config/hypr/hyprland.conf`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Required for clipboard history integration
|
|
||||||
exec-once = bash -c "wl-paste --watch cliphist store &"
|
|
||||||
|
|
||||||
# Recommended (must install polkit-mate beforehand) for elevation prompts
|
|
||||||
exec-once = /usr/lib/mate-polkit/polkit-mate-authentication-agent-1
|
|
||||||
# This may be a different path on different distributions, the above is for the arch linux mate-polkit package
|
|
||||||
|
|
||||||
# Starts DankShell
|
|
||||||
exec-once = dms run
|
|
||||||
|
|
||||||
# Dank keybinds
|
|
||||||
# 1. These should not be in conflict with any pre-existing keybindings
|
|
||||||
# 2. You need to merge them with your existing config if you want to use these
|
|
||||||
# 3. You can change the keys to whatever you want, if you prefer something different
|
|
||||||
# 4. For the increment/decrement ones you can change the steps to whatever you like too
|
|
||||||
|
|
||||||
# Application and system controls
|
|
||||||
bind = SUPER, Space, exec, dms ipc call spotlight toggle
|
|
||||||
bind = SUPER, V, exec, dms ipc call clipboard toggle
|
|
||||||
bind = SUPER, M, exec, dms ipc call processlist toggle
|
|
||||||
bind = SUPER, N, exec, dms ipc call notifications toggle
|
|
||||||
bind = SUPER, comma, exec, dms ipc call settings toggle
|
|
||||||
bind = SUPER, P, exec, dms ipc call notepad toggle
|
|
||||||
bind = SUPERALT, L, exec, dms ipc call lock lock
|
|
||||||
bind = SUPER, X, exec, dms ipc call powermenu toggle
|
|
||||||
bind = SUPER, C, exec, dms ipc call control-center toggle
|
|
||||||
|
|
||||||
# Audio controls (function keys)
|
|
||||||
bindl = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
|
||||||
bindl = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
|
||||||
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
|
||||||
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
|
||||||
|
|
||||||
# Brightness controls (function keys)
|
|
||||||
bindl = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
|
||||||
# You can override the default device for e.g. keyboards by adding the device name to the last param
|
|
||||||
bindl = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
|
||||||
|
|
||||||
# Night mode toggle
|
|
||||||
bind = SUPERSHIFT, N, exec, dms ipc call night toggle
|
|
||||||
```
|
|
||||||
|
|
||||||
## IPC Commands
|
|
||||||
|
|
||||||
Control everything from the command line, or via keybinds. For comprehensive documentation of all available IPC commands, see [docs/IPC.md](docs/IPC.md).
|
|
||||||
|
|
||||||
### Audio control
|
|
||||||
```bash
|
|
||||||
dms ipc call audio setvolume 50
|
|
||||||
dms ipc call audio mute
|
|
||||||
```
|
|
||||||
### Launch applications
|
|
||||||
```bash
|
|
||||||
dms ipc call spotlight toggle
|
dms ipc call spotlight toggle
|
||||||
dms ipc call notepad toggle
|
dms ipc call audio setvolume 50
|
||||||
dms ipc call processlist toggle
|
|
||||||
dms ipc call powermenu toggle
|
|
||||||
```
|
|
||||||
### System control
|
|
||||||
```
|
|
||||||
dms ipc call wallpaper set /path/to/image.jpg
|
dms ipc call wallpaper set /path/to/image.jpg
|
||||||
dms ipc call theme toggle
|
dms brightness list # List available displays
|
||||||
dms ipc call night toggle
|
dms plugins search # Browse plugin registry
|
||||||
dms ipc call lock lock
|
|
||||||
```
|
|
||||||
### Media control
|
|
||||||
```
|
|
||||||
dms ipc call mpris playPause
|
|
||||||
dms ipc call mpris next
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Theming
|
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
|
||||||
|
|
||||||
dms will spawn a matugen process on theme changes to generate color palettes for installed and supported apps. If you do not want these files generated, you can set the env variable `DMS_DISABLE_MATUGEN=1` to disable it entirely.
|
## Documentation
|
||||||
|
|
||||||
### Custom Themes
|
- **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)
|
||||||
|
|
||||||
DankMaterialShell supports custom color themes! You can create your own Material Design 3 color schemes or use pre-made themes like Cyberpunk Electric, Hotline Miami, and Miami Vice.
|
## Development
|
||||||
|
|
||||||
For detailed instructions on creating and using custom themes, see [docs/CUSTOM_THEMES.md](docs/CUSTOM_THEMES.md).
|
See component-specific documentation:
|
||||||
|
|
||||||
### System App Integration
|
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
|
||||||
|
- **[backend/](backend/)** - Go backend, CLI tools, and system integration
|
||||||
|
- **[distro/](distro/)** - Distribution packaging
|
||||||
|
- **[nix/](nix/)** - NixOS and home-manager modules
|
||||||
|
|
||||||
There's two toggles in the appearance section of settings, for GTK and QT apps.
|
### Building from Source
|
||||||
|
|
||||||
These settings will override some local GTK and QT configuration files, you can still integrate auto-theming if you do not wish DankShell to mess with your QTCT/GTK files.
|
|
||||||
|
|
||||||
No matter what when matugen is enabled the files will be created on wallpaper changes:
|
|
||||||
|
|
||||||
- ~/.config/gtk-3.0/dank-colors.css
|
|
||||||
- ~/.config/gtk-4.0/dank-colors.css
|
|
||||||
- ~/.config/qt6ct/colors/matugen.conf
|
|
||||||
- ~/.config/qt5ct/colors/matugen.conf
|
|
||||||
|
|
||||||
If you do not like our theme path, you can integrate this with other GTK themes, matugen themes, etc.
|
|
||||||
|
|
||||||
#### GTK Apps
|
|
||||||
|
|
||||||
1. Install [Colloid](https://github.com/vinceliuice/Colloid-gtk-theme)
|
|
||||||
|
|
||||||
Colloid is a hard requirement for the auto-theming because of how it integrates with colloid css files, however you can integrate auto-theming with other themes, you just have to do it manually (so leave the toggle OFF in settings)
|
|
||||||
|
|
||||||
It will still create `~/.config/gtk-3.0/4.0/dank-colors.css` on theme updates, these you can import into other compatible GTK themes.
|
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
```bash
|
```bash
|
||||||
# Some default install settings for colloid
|
cd backend
|
||||||
./install.sh -s standard -l --tweaks normal
|
make # Build dms CLI
|
||||||
|
make dankinstall # Build installer
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure in `~/.config/gtk-3.0/settings.ini` and `~/.config/gtk-4.0/settings.ini`:
|
**Shell:**
|
||||||
|
```bash
|
||||||
```ini
|
quickshell -p quickshell/
|
||||||
[Settings]
|
|
||||||
gtk-theme-name=Colloid
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### QT: basic gtk3 based theme (Option 1)
|
**NixOS:**
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||||
|
|
||||||
If you mostly use gtk apps, you'll probably be happy to just set the QT platform theme to gtk3.
|
# Use in home-manager or NixOS configuration
|
||||||
|
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
|
||||||
```kdl
|
|
||||||
environment {
|
|
||||||
// Add to existing environment block
|
|
||||||
QT_QPA_PLATFORMTHEME "gtk3"
|
|
||||||
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### QT: better theming (Option 2)
|
|
||||||
|
|
||||||
1. Install qt6ct-kde
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Arch
|
|
||||||
paru -S qt6ct-kde
|
|
||||||
```
|
|
||||||
|
|
||||||
*I'm not sure what it is on other distros, but you can manually install via instructions provides on [qt6ct-kde github](https://www.opencode.net/trialuser/qt6ct)
|
|
||||||
|
|
||||||
2. **Configure Environment in niri**
|
|
||||||
|
|
||||||
```kdl
|
|
||||||
// Add to existing environment block
|
|
||||||
QT_QPA_PLATFORMTHEME "qt6ct"
|
|
||||||
QT_QPA_PLATFORMTHEME_QT6 "qt6ct"
|
|
||||||
```
|
|
||||||
|
|
||||||
You'll have to restart your session for themes to take effect.
|
|
||||||
|
|
||||||
Nevigate to dms settings -> themes & colors -> and click "Apply QT Themes"
|
|
||||||
|
|
||||||
#### Firefox
|
|
||||||
|
|
||||||
There are two theme paths for Firefox, using with [pywalfox](https://github.com/Frewacom/pywalfox) or [material fox](https://github.com/edelvarden/material-fox-updated)
|
|
||||||
|
|
||||||
**(Option 1) - pywalfox**
|
|
||||||
|
|
||||||
1. **Install [pywalfox](https://github.com/Frewacom/pywalfox)** on system.
|
|
||||||
- Available in AUR via `paru -S python-pywalfox`
|
|
||||||
|
|
||||||
2. **Install [pywalfox extension](https://addons.mozilla.org/firefox/addon/pywalfox/)** in firefox.
|
|
||||||
|
|
||||||
3. **Restart dms and create symlink** to generate palette and then enable dank colors.
|
|
||||||
- Run `ln -sf ~/.cache/wal/dank-pywalfox.json ~/.cache/wal/colors.json`
|
|
||||||
|
|
||||||
|
|
||||||
**(Option 2) - Chrome-like theme with dynamic colors**
|
|
||||||
|
|
||||||
Firefox does use the GTK3 theme, but it doesn't look that good on the stock theme IMO. A separate matugen css is generated for the [material fox](https://github.com/edelvarden/material-fox-updated) theme, you can configure that theme with dynamic colors by following the steps below.
|
|
||||||
|
|
||||||
1. **In firefox, navigate to `about:config`**
|
|
||||||
- set `toolkit.legacyuserprofilecustomizations.stylesheets` to `true`
|
|
||||||
- set `svg.context-properties.content.enabled` to `true`
|
|
||||||
- Create a new property called `userChrome.theme-material` and type `boolean`
|
|
||||||
- set to `true`
|
|
||||||
|
|
||||||
<details><summary><strong>Expand for firefox screenshots</strong></summary>
|
|
||||||
<img width="1262" height="104" alt="image" src="https://github.com/user-attachments/assets/4bca43d1-5735-4401-9b91-5ee4f0b1e357" />
|
|
||||||
<img width="1262" height="104" alt="image" src="https://github.com/user-attachments/assets/348d37e0-5c6c-4db8-b7c9-89cabf282c25" />
|
|
||||||
<img width="1244" height="106" alt="image" src="https://github.com/user-attachments/assets/75fd4972-bc4a-4657-b756-b31ef8061b3b" />
|
|
||||||
</details>
|
|
||||||
|
|
||||||
2. **Install material fox theme**
|
|
||||||
```bash
|
|
||||||
# Find Firefox profile directory
|
|
||||||
export PROFILE_DIR=$(find ~/.mozilla/firefox -maxdepth 1 -type d -name "*.default-release" | head -n 1)
|
|
||||||
|
|
||||||
# Download, extract to profile dir, and cleanup
|
|
||||||
curl -L -o "$PROFILE_DIR/chrome.zip" https://github.com/edelvarden/material-fox-updated/releases/download/v2.0.0/chrome.zip
|
|
||||||
unzip -o "$PROFILE_DIR/chrome.zip" -d "$PROFILE_DIR"
|
|
||||||
rm "$PROFILE_DIR/chrome.zip"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Configure dynamic colors for material fox theme**
|
|
||||||
```bash
|
|
||||||
export PROFILE_DIR=$(find ~/.mozilla/firefox -maxdepth 1 -type d -name "*.default-release" | head -n 1)
|
|
||||||
rm -f "$PROFILE_DIR/chrome/theme-material-blue.css"
|
|
||||||
ln -sf ~/.config/DankMaterialShell/firefox.css "$PROFILE_DIR/chrome/theme-material-blue.css"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Terminal Integration
|
|
||||||
|
|
||||||
The matugen integration will automatically generate new colors for certain apps only if they are installed.
|
|
||||||
|
|
||||||
You can enable the dynamic color schemes in supported terminal apps by modifying their configurations:
|
|
||||||
|
|
||||||
**Ghostty**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "config-file = ./config-dankcolors" >> ~/.config/ghostty/config
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to disable excessive config reloaded popup sin ghostty, you may wish to also add this:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# These are the default danklinux options, if you still want config reloaded and copied to clipboard popups you can skip it.
|
|
||||||
echo "app-notifications = no-clipboard-copy,no-config-reload" >> ~/.config/ghostty/config
|
|
||||||
```
|
|
||||||
|
|
||||||
**kitty**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "include dank-theme.conf" >> ~/.config/kitty/kitty.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Calendar Setup
|
|
||||||
|
|
||||||
Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration:
|
|
||||||
|
|
||||||
<details><summary>Configuration Steps</summary>
|
|
||||||
|
|
||||||
**Install dependencies:**
|
|
||||||
|
|
||||||
#### Arch
|
|
||||||
```bash
|
|
||||||
sudo pacman -S vdirsyncer khal python-aiohttp-oauthlib
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Fedora
|
|
||||||
```bash
|
|
||||||
sudo dnf install python3-vdirsyncer khal python3-aiohttp-oauthlib
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configure vdirsyncer** (`~/.vdirsyncer/config`):
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[general]
|
|
||||||
status_path = "~/.calendars/status"
|
|
||||||
|
|
||||||
[pair personal_sync]
|
|
||||||
a = "personal"
|
|
||||||
b = "personallocal"
|
|
||||||
collections = ["from a", "from b"]
|
|
||||||
conflict_resolution = "a wins"
|
|
||||||
metadata = ["color"]
|
|
||||||
|
|
||||||
[storage personal]
|
|
||||||
type = "google_calendar"
|
|
||||||
token_file = "~/.vdirsyncer/google_calendar_token"
|
|
||||||
client_id = "your_client_id"
|
|
||||||
client_secret = "your_client_secret"
|
|
||||||
|
|
||||||
[storage personallocal]
|
|
||||||
type = "filesystem"
|
|
||||||
path = "~/.calendars/Personal"
|
|
||||||
fileext = ".ics"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Setup sync:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vdirsyncer sync
|
|
||||||
khal configure
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Auto-sync every 5 minutes
|
|
||||||
```bash
|
|
||||||
crontab -e
|
|
||||||
# Add: */5 * * * * /usr/bin/vdirsyncer sync
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
All settings are configurable in
|
|
||||||
```
|
|
||||||
~/.config/DankMaterialShell/settings.json`, or more intuitively the built-in settings modal.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key configuration areas:**
|
|
||||||
|
|
||||||
- Widget positioning and behavior
|
|
||||||
- Theme and color preferences
|
|
||||||
- Time format, weather units and location
|
|
||||||
- Light/Dark modes
|
|
||||||
- Wallpaper and Profile picture
|
|
||||||
- Dock enable/disable and various tunes.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**Common issues:**
|
|
||||||
|
|
||||||
- **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material`
|
|
||||||
- **No dynamic theming:** Install matugen and enable in settings
|
|
||||||
- **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME
|
|
||||||
- **Calendar not syncing:** Check vdirsyncer credentials and network connectivity
|
|
||||||
|
|
||||||
**Getting help:**
|
|
||||||
|
|
||||||
- Check the [issues](https://github.com/AvengeMedia/DankMaterialShell/issues) for known problems
|
|
||||||
- Re-run the shell with `dms kill && dms run` to capture logs.
|
|
||||||
- Join the niri community for compositor-specific questions
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, theme improvements, or documentation updates - all help is appreciated.
|
Contributions welcome. Bug fixes, widgets, features, documentation, and plugins all help.
|
||||||
|
|
||||||
**Areas that need attention:**
|
1. Fork the repository
|
||||||
|
2. Make your changes
|
||||||
|
3. Test thoroughly
|
||||||
|
4. Open a pull request
|
||||||
|
|
||||||
- More widget options and customization
|
For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs).
|
||||||
- Additional compositor compatibility
|
|
||||||
- Performance optimizations
|
|
||||||
- Documentation and examples
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
|
- [quickshell](https://quickshell.org/) - Shell framework
|
||||||
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
|
- [niri](https://github.com/YaLTeR/niri) - Scrolling window manager
|
||||||
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
|
- [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
||||||
- [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.
|
- [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
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import "../Common/fzf.js" as Fzf
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
|
|
||||||
|
|
||||||
function searchApplications(query) {
|
|
||||||
if (!query || query.length === 0)
|
|
||||||
return applications
|
|
||||||
if (applications.length === 0)
|
|
||||||
return []
|
|
||||||
|
|
||||||
const queryLower = query.toLowerCase().trim()
|
|
||||||
const scoredApps = []
|
|
||||||
|
|
||||||
for (const app of applications) {
|
|
||||||
const name = (app.name || "").toLowerCase()
|
|
||||||
const genericName = (app.genericName || "").toLowerCase()
|
|
||||||
const comment = (app.comment || "").toLowerCase()
|
|
||||||
const keywords = app.keywords ? app.keywords.map(k => k.toLowerCase()) : []
|
|
||||||
|
|
||||||
let score = 0
|
|
||||||
let matched = false
|
|
||||||
|
|
||||||
const nameWords = name.trim().split(/\s+/).filter(w => w.length > 0)
|
|
||||||
const containsAsWord = nameWords.includes(queryLower)
|
|
||||||
const startsWithAsWord = nameWords.some(word => word.startsWith(queryLower))
|
|
||||||
|
|
||||||
if (name === queryLower) {
|
|
||||||
score = 10000
|
|
||||||
matched = true
|
|
||||||
} else if (containsAsWord) {
|
|
||||||
score = 9500 + (100 - Math.min(name.length, 100))
|
|
||||||
matched = true
|
|
||||||
} else if (name.startsWith(queryLower)) {
|
|
||||||
score = 9000 + (100 - Math.min(name.length, 100))
|
|
||||||
matched = true
|
|
||||||
} else if (startsWithAsWord) {
|
|
||||||
score = 8500 + (100 - Math.min(name.length, 100))
|
|
||||||
matched = true
|
|
||||||
} else if (name.includes(queryLower)) {
|
|
||||||
score = 8000 + (100 - Math.min(name.length, 100))
|
|
||||||
matched = true
|
|
||||||
} else if (keywords.length > 0) {
|
|
||||||
for (const keyword of keywords) {
|
|
||||||
if (keyword === queryLower) {
|
|
||||||
score = 6000
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
} else if (keyword.startsWith(queryLower)) {
|
|
||||||
score = 5500
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
} else if (keyword.includes(queryLower)) {
|
|
||||||
score = 5000
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!matched && genericName.includes(queryLower)) {
|
|
||||||
score = 4000
|
|
||||||
matched = true
|
|
||||||
} else if (!matched && comment.includes(queryLower)) {
|
|
||||||
score = 3000
|
|
||||||
matched = true
|
|
||||||
} else if (!matched) {
|
|
||||||
const nameFinder = new Fzf.Finder([app], {
|
|
||||||
"selector": a => a.name || "",
|
|
||||||
"casing": "case-insensitive",
|
|
||||||
"fuzzy": "v2"
|
|
||||||
})
|
|
||||||
const fuzzyResults = nameFinder.find(query)
|
|
||||||
if (fuzzyResults.length > 0 && fuzzyResults[0].score > 0) {
|
|
||||||
score = Math.min(fuzzyResults[0].score, 2000)
|
|
||||||
matched = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matched) {
|
|
||||||
scoredApps.push({
|
|
||||||
"app": app,
|
|
||||||
"score": score
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scoredApps.sort((a, b) => b.score - a.score)
|
|
||||||
return scoredApps.slice(0, 50).map(item => item.app)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCategoriesForApp(app) {
|
|
||||||
if (!app?.categories)
|
|
||||||
return []
|
|
||||||
|
|
||||||
const categoryMap = {
|
|
||||||
"AudioVideo": "Media",
|
|
||||||
"Audio": "Media",
|
|
||||||
"Video": "Media",
|
|
||||||
"Development": "Development",
|
|
||||||
"TextEditor": "Development",
|
|
||||||
"IDE": "Development",
|
|
||||||
"Education": "Education",
|
|
||||||
"Game": "Games",
|
|
||||||
"Graphics": "Graphics",
|
|
||||||
"Photography": "Graphics",
|
|
||||||
"Network": "Internet",
|
|
||||||
"WebBrowser": "Internet",
|
|
||||||
"Email": "Internet",
|
|
||||||
"Office": "Office",
|
|
||||||
"WordProcessor": "Office",
|
|
||||||
"Spreadsheet": "Office",
|
|
||||||
"Presentation": "Office",
|
|
||||||
"Science": "Science",
|
|
||||||
"Settings": "Settings",
|
|
||||||
"System": "System",
|
|
||||||
"Utility": "Utilities",
|
|
||||||
"Accessories": "Utilities",
|
|
||||||
"FileManager": "Utilities",
|
|
||||||
"TerminalEmulator": "Utilities"
|
|
||||||
}
|
|
||||||
|
|
||||||
const mappedCategories = new Set()
|
|
||||||
|
|
||||||
for (const cat of app.categories) {
|
|
||||||
if (categoryMap[cat])
|
|
||||||
mappedCategories.add(categoryMap[cat])
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(mappedCategories)
|
|
||||||
}
|
|
||||||
|
|
||||||
property var categoryIcons: ({
|
|
||||||
"All": "apps",
|
|
||||||
"Media": "music_video",
|
|
||||||
"Development": "code",
|
|
||||||
"Games": "sports_esports",
|
|
||||||
"Graphics": "photo_library",
|
|
||||||
"Internet": "web",
|
|
||||||
"Office": "content_paste",
|
|
||||||
"Settings": "settings",
|
|
||||||
"System": "host",
|
|
||||||
"Utilities": "build"
|
|
||||||
})
|
|
||||||
|
|
||||||
function getCategoryIcon(category) {
|
|
||||||
return categoryIcons[category] || "folder"
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllCategories() {
|
|
||||||
const categories = new Set(["All"])
|
|
||||||
|
|
||||||
for (const app of applications) {
|
|
||||||
const appCategories = getCategoriesForApp(app)
|
|
||||||
appCategories.forEach(cat => categories.add(cat))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(categories).sort()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAppsInCategory(category) {
|
|
||||||
if (category === "All") {
|
|
||||||
return applications
|
|
||||||
}
|
|
||||||
|
|
||||||
return applications.filter(app => {
|
|
||||||
const appCategories = getCategoriesForApp(app)
|
|
||||||
return appCategories.includes(category)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
|
||||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
|
||||||
|
|
||||||
signal volumeChanged
|
|
||||||
signal micMuteChanged
|
|
||||||
|
|
||||||
function displayName(node) {
|
|
||||||
if (!node) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.properties && node.properties["device.description"]) {
|
|
||||||
return node.properties["device.description"]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.description && node.description !== node.name) {
|
|
||||||
return node.description
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.nickname && node.nickname !== node.name) {
|
|
||||||
return node.nickname
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.name.includes("analog-stereo")) {
|
|
||||||
return "Built-in Speakers"
|
|
||||||
}
|
|
||||||
if (node.name.includes("bluez")) {
|
|
||||||
return "Bluetooth Audio"
|
|
||||||
}
|
|
||||||
if (node.name.includes("usb")) {
|
|
||||||
return "USB Audio"
|
|
||||||
}
|
|
||||||
if (node.name.includes("hdmi")) {
|
|
||||||
return "HDMI Audio"
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.name
|
|
||||||
}
|
|
||||||
|
|
||||||
function subtitle(name) {
|
|
||||||
if (!name) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('usb-')) {
|
|
||||||
if (name.includes('SteelSeries')) {
|
|
||||||
return "USB Gaming Headset"
|
|
||||||
}
|
|
||||||
if (name.includes('Generic')) {
|
|
||||||
return "USB Audio Device"
|
|
||||||
}
|
|
||||||
return "USB Audio"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('pci-')) {
|
|
||||||
if (name.includes('01_00.1') || name.includes('01:00.1')) {
|
|
||||||
return "NVIDIA GPU Audio"
|
|
||||||
}
|
|
||||||
return "PCI Audio"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('bluez')) {
|
|
||||||
return "Bluetooth Audio"
|
|
||||||
}
|
|
||||||
if (name.includes('analog')) {
|
|
||||||
return "Built-in Audio"
|
|
||||||
}
|
|
||||||
if (name.includes('hdmi')) {
|
|
||||||
return "HDMI Audio"
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
PwObjectTracker {
|
|
||||||
objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVolume(percentage) {
|
|
||||||
if (!root.sink?.audio) {
|
|
||||||
return "No audio sink available"
|
|
||||||
}
|
|
||||||
|
|
||||||
const clampedVolume = Math.max(0, Math.min(100, percentage))
|
|
||||||
root.sink.audio.volume = clampedVolume / 100
|
|
||||||
root.volumeChanged()
|
|
||||||
return `Volume set to ${clampedVolume}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMute() {
|
|
||||||
if (!root.sink?.audio) {
|
|
||||||
return "No audio sink available"
|
|
||||||
}
|
|
||||||
|
|
||||||
root.sink.audio.muted = !root.sink.audio.muted
|
|
||||||
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMicVolume(percentage) {
|
|
||||||
if (!root.source?.audio) {
|
|
||||||
return "No audio source available"
|
|
||||||
}
|
|
||||||
|
|
||||||
const clampedVolume = Math.max(0, Math.min(100, percentage))
|
|
||||||
root.source.audio.volume = clampedVolume / 100
|
|
||||||
return `Microphone volume set to ${clampedVolume}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMicMute() {
|
|
||||||
if (!root.source?.audio) {
|
|
||||||
return "No audio source available"
|
|
||||||
}
|
|
||||||
|
|
||||||
root.source.audio.muted = !root.source.audio.muted
|
|
||||||
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "audio"
|
|
||||||
|
|
||||||
function setvolume(percentage: string): string {
|
|
||||||
return root.setVolume(parseInt(percentage))
|
|
||||||
}
|
|
||||||
|
|
||||||
function increment(step: string): string {
|
|
||||||
if (!root.sink?.audio) {
|
|
||||||
return "No audio sink available"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.sink.audio.muted) {
|
|
||||||
root.sink.audio.muted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentVolume = Math.round(root.sink.audio.volume * 100)
|
|
||||||
const stepValue = parseInt(step || "5")
|
|
||||||
const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue))
|
|
||||||
|
|
||||||
root.sink.audio.volume = newVolume / 100
|
|
||||||
root.volumeChanged()
|
|
||||||
return `Volume increased to ${newVolume}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
function decrement(step: string): string {
|
|
||||||
if (!root.sink?.audio) {
|
|
||||||
return "No audio sink available"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.sink.audio.muted) {
|
|
||||||
root.sink.audio.muted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentVolume = Math.round(root.sink.audio.volume * 100)
|
|
||||||
const stepValue = parseInt(step || "5")
|
|
||||||
const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue))
|
|
||||||
|
|
||||||
root.sink.audio.volume = newVolume / 100
|
|
||||||
root.volumeChanged()
|
|
||||||
return `Volume decreased to ${newVolume}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
function mute(): string {
|
|
||||||
const result = root.toggleMute()
|
|
||||||
root.volumeChanged()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function setmic(percentage: string): string {
|
|
||||||
return root.setMicVolume(parseInt(percentage))
|
|
||||||
}
|
|
||||||
|
|
||||||
function micmute(): string {
|
|
||||||
const result = root.toggleMicMute()
|
|
||||||
root.micMuteChanged()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function status(): string {
|
|
||||||
let result = "Audio Status:\n"
|
|
||||||
|
|
||||||
if (root.sink?.audio) {
|
|
||||||
const volume = Math.round(root.sink.audio.volume * 100)
|
|
||||||
const muteStatus = root.sink.audio.muted ? " (muted)" : ""
|
|
||||||
result += `Output: ${volume}%${muteStatus}\n`
|
|
||||||
} else {
|
|
||||||
result += "Output: No sink available\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.source?.audio) {
|
|
||||||
const micVolume = Math.round(root.source.audio.volume * 100)
|
|
||||||
const muteStatus = root.source.audio.muted ? " (muted)" : ""
|
|
||||||
result += `Input: ${micVolume}%${muteStatus}`
|
|
||||||
} else {
|
|
||||||
result += "Input: No source available"
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.UPower
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property UPowerDevice device: {
|
|
||||||
UPower.devices.values.find(dev => dev.isLaptopBattery) || null
|
|
||||||
}
|
|
||||||
readonly property bool batteryAvailable: device && device.ready
|
|
||||||
readonly property real batteryLevel: batteryAvailable ? Math.round(device.percentage * 100) : 0
|
|
||||||
readonly property bool isCharging: batteryAvailable && device.state === UPowerDeviceState.Charging && device.changeRate > 0
|
|
||||||
readonly property bool isPluggedIn: batteryAvailable && (device.state !== UPowerDeviceState.Discharging && device.state !== UPowerDeviceState.Empty)
|
|
||||||
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
|
|
||||||
readonly property string batteryHealth: {
|
|
||||||
if (!batteryAvailable) {
|
|
||||||
return "N/A"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.healthSupported && device.healthPercentage > 0) {
|
|
||||||
return `${Math.round(device.healthPercentage)}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
return "N/A"
|
|
||||||
}
|
|
||||||
readonly property real batteryCapacity: batteryAvailable && device.energyCapacity > 0 ? device.energyCapacity : 0
|
|
||||||
readonly property string batteryStatus: {
|
|
||||||
if (!batteryAvailable) {
|
|
||||||
return "No Battery"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.state === UPowerDeviceState.Charging && device.changeRate <= 0) {
|
|
||||||
return "Plugged In"
|
|
||||||
}
|
|
||||||
|
|
||||||
return UPowerDeviceState.toString(device.state)
|
|
||||||
}
|
|
||||||
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver)
|
|
||||||
|
|
||||||
readonly property var bluetoothDevices: {
|
|
||||||
const btDevices = []
|
|
||||||
const bluetoothTypes = [UPowerDeviceType.BluetoothGeneric, UPowerDeviceType.Headphones, UPowerDeviceType.Headset, UPowerDeviceType.Keyboard, UPowerDeviceType.Mouse, UPowerDeviceType.Speakers]
|
|
||||||
|
|
||||||
for (var i = 0; i < UPower.devices.count; i++) {
|
|
||||||
const dev = UPower.devices.get(i)
|
|
||||||
if (dev && dev.ready && bluetoothTypes.includes(dev.type)) {
|
|
||||||
btDevices.push({
|
|
||||||
"name": dev.model || UPowerDeviceType.toString(dev.type),
|
|
||||||
"percentage": Math.round(dev.percentage),
|
|
||||||
"type": dev.type
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return btDevices
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimeRemaining() {
|
|
||||||
if (!batteryAvailable) {
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty
|
|
||||||
|
|
||||||
if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400) {
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
const hours = Math.floor(timeSeconds / 3600)
|
|
||||||
const minutes = Math.floor((timeSeconds % 3600) / 60)
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${minutes}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${minutes}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBatteryIcon() {
|
|
||||||
if (!batteryAvailable) {
|
|
||||||
return "power"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCharging) {
|
|
||||||
if (batteryLevel >= 90) {
|
|
||||||
return "battery_charging_full"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 80) {
|
|
||||||
return "battery_charging_90"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 60) {
|
|
||||||
return "battery_charging_80"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 50) {
|
|
||||||
return "battery_charging_60"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 30) {
|
|
||||||
return "battery_charging_50"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 20) {
|
|
||||||
return "battery_charging_30"
|
|
||||||
}
|
|
||||||
return "battery_charging_20"
|
|
||||||
}
|
|
||||||
if (isPluggedIn) {
|
|
||||||
if (batteryLevel >= 90) {
|
|
||||||
return "battery_charging_full"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 80) {
|
|
||||||
return "battery_charging_90"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 60) {
|
|
||||||
return "battery_charging_80"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 50) {
|
|
||||||
return "battery_charging_60"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 30) {
|
|
||||||
return "battery_charging_50"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 20) {
|
|
||||||
return "battery_charging_30"
|
|
||||||
}
|
|
||||||
return "battery_charging_20"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 95) {
|
|
||||||
return "battery_full"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 85) {
|
|
||||||
return "battery_6_bar"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 70) {
|
|
||||||
return "battery_5_bar"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 55) {
|
|
||||||
return "battery_4_bar"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 40) {
|
|
||||||
return "battery_3_bar"
|
|
||||||
}
|
|
||||||
if (batteryLevel >= 25) {
|
|
||||||
return "battery_2_bar"
|
|
||||||
}
|
|
||||||
return "battery_1_bar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isHyprland: false
|
|
||||||
property bool isNiri: false
|
|
||||||
property string compositor: "unknown"
|
|
||||||
|
|
||||||
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
|
|
||||||
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
|
|
||||||
|
|
||||||
property bool useNiriSorting: isNiri && NiriService
|
|
||||||
|
|
||||||
property var sortedToplevels: {
|
|
||||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useNiriSorting) {
|
|
||||||
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHyprland) {
|
|
||||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
|
|
||||||
|
|
||||||
const sortedHyprland = hyprlandToplevels.sort((a, b) => {
|
|
||||||
if (a.monitor && b.monitor) {
|
|
||||||
const monitorCompare = a.monitor.name.localeCompare(b.monitor.name)
|
|
||||||
if (monitorCompare !== 0) {
|
|
||||||
return monitorCompare
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.workspace && b.workspace) {
|
|
||||||
const workspaceCompare = a.workspace.id - b.workspace.id
|
|
||||||
if (workspaceCompare !== 0) {
|
|
||||||
return workspaceCompare
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.lastIpcObject && b.lastIpcObject && a.lastIpcObject.at && b.lastIpcObject.at) {
|
|
||||||
const aX = a.lastIpcObject.at[0]
|
|
||||||
const bX = b.lastIpcObject.at[0]
|
|
||||||
const aY = a.lastIpcObject.at[1]
|
|
||||||
const bY = b.lastIpcObject.at[1]
|
|
||||||
|
|
||||||
const xCompare = aX - bX
|
|
||||||
if (Math.abs(xCompare) > 10) {
|
|
||||||
return xCompare
|
|
||||||
}
|
|
||||||
return aY - bY
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.lastIpcObject && !b.lastIpcObject) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (!a.lastIpcObject && b.lastIpcObject) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.title && b.title) {
|
|
||||||
return a.title.localeCompare(b.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
return sortedHyprland.map(hyprToplevel => hyprToplevel.wayland).filter(wayland => wayland !== null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ToplevelManager.toplevels.values
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
detectCompositor()
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterCurrentWorkspace(toplevels, screen) {
|
|
||||||
if (useNiriSorting) {
|
|
||||||
return NiriService.filterCurrentWorkspace(toplevels, screen)
|
|
||||||
}
|
|
||||||
if (isHyprland) {
|
|
||||||
return filterHyprlandCurrentWorkspace(toplevels, screen)
|
|
||||||
}
|
|
||||||
return toplevels
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterHyprlandCurrentWorkspace(toplevels, screenName) {
|
|
||||||
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) {
|
|
||||||
return toplevels
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentWorkspaceId = null
|
|
||||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
|
|
||||||
|
|
||||||
for (const hyprToplevel of hyprlandToplevels) {
|
|
||||||
if (hyprToplevel.monitor && hyprToplevel.monitor.name === screenName && hyprToplevel.workspace) {
|
|
||||||
if (hyprToplevel.activated) {
|
|
||||||
currentWorkspaceId = hyprToplevel.workspace.id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (currentWorkspaceId === null) {
|
|
||||||
currentWorkspaceId = hyprToplevel.workspace.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentWorkspaceId === null && Hyprland.workspaces) {
|
|
||||||
const workspaces = Array.from(Hyprland.workspaces.values)
|
|
||||||
for (const workspace of workspaces) {
|
|
||||||
if (workspace.monitor && workspace.monitor === screenName) {
|
|
||||||
if (Hyprland.focusedWorkspace && workspace.id === Hyprland.focusedWorkspace.id) {
|
|
||||||
currentWorkspaceId = workspace.id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (currentWorkspaceId === null) {
|
|
||||||
currentWorkspaceId = workspace.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentWorkspaceId === null) {
|
|
||||||
return toplevels
|
|
||||||
}
|
|
||||||
|
|
||||||
return toplevels.filter(toplevel => {
|
|
||||||
for (const hyprToplevel of hyprlandToplevels) {
|
|
||||||
if (hyprToplevel.wayland === toplevel) {
|
|
||||||
return hyprToplevel.workspace && hyprToplevel.workspace.id === currentWorkspaceId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCompositor() {
|
|
||||||
if (hyprlandSignature && hyprlandSignature.length > 0) {
|
|
||||||
isHyprland = true
|
|
||||||
isNiri = false
|
|
||||||
compositor = "hyprland"
|
|
||||||
console.log("CompositorService: Detected Hyprland")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (niriSocket && niriSocket.length > 0) {
|
|
||||||
niriSocketCheck.running = true
|
|
||||||
} else {
|
|
||||||
isHyprland = false
|
|
||||||
isNiri = false
|
|
||||||
compositor = "unknown"
|
|
||||||
console.warn("CompositorService: No compositor detected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function powerOffMonitors() {
|
|
||||||
if (isNiri) {
|
|
||||||
return NiriService.powerOffMonitors()
|
|
||||||
}
|
|
||||||
if (isHyprland) {
|
|
||||||
return Hyprland.dispatch("dpms off")
|
|
||||||
}
|
|
||||||
console.warn("CompositorService: Cannot power off monitors, unknown compositor")
|
|
||||||
}
|
|
||||||
|
|
||||||
function powerOnMonitors() {
|
|
||||||
if (isNiri) {
|
|
||||||
return NiriService.powerOnMonitors()
|
|
||||||
}
|
|
||||||
if (isHyprland) {
|
|
||||||
return Hyprland.dispatch("dpms on")
|
|
||||||
}
|
|
||||||
console.warn("CompositorService: Cannot power on monitors, unknown compositor")
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: niriSocketCheck
|
|
||||||
command: ["test", "-S", root.niriSocket]
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
root.isNiri = true
|
|
||||||
root.isHyprland = false
|
|
||||||
root.compositor = "niri"
|
|
||||||
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
|
|
||||||
} else {
|
|
||||||
root.isHyprland = false
|
|
||||||
root.isNiri = true
|
|
||||||
root.compositor = "niri"
|
|
||||||
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,979 +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 ddcDevices: []
|
|
||||||
property var deviceBrightness: ({})
|
|
||||||
property var ddcPendingInit: ({})
|
|
||||||
property string currentDevice: ""
|
|
||||||
property string lastIpcDevice: ""
|
|
||||||
property bool ddcAvailable: false
|
|
||||||
property var ddcInitQueue: []
|
|
||||||
property bool skipDdcRead: false
|
|
||||||
property int brightnessLevel: {
|
|
||||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
|
||||||
if (!deviceToUse) {
|
|
||||||
return 50
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDeviceBrightness(deviceToUse)
|
|
||||||
}
|
|
||||||
property int maxBrightness: 100
|
|
||||||
property bool brightnessInitialized: false
|
|
||||||
|
|
||||||
signal brightnessChanged
|
|
||||||
signal deviceSwitched
|
|
||||||
|
|
||||||
property bool nightModeActive: nightModeEnabled
|
|
||||||
|
|
||||||
property bool nightModeEnabled: false
|
|
||||||
property bool automationAvailable: false
|
|
||||||
property bool geoclueAvailable: false
|
|
||||||
property bool isAutomaticNightTime: false
|
|
||||||
|
|
||||||
function buildGammastepCommand(gammastepArgs) {
|
|
||||||
const commandStr = "pkill gammastep; " + ["gammastep"].concat(gammastepArgs).join(" ")
|
|
||||||
return ["sh", "-c", commandStr]
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBrightnessInternal(percentage, device) {
|
|
||||||
const clampedValue = Math.max(1, Math.min(100, percentage))
|
|
||||||
const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice())
|
|
||||||
|
|
||||||
if (actualDevice) {
|
|
||||||
const newBrightness = Object.assign({}, deviceBrightness)
|
|
||||||
newBrightness[actualDevice] = clampedValue
|
|
||||||
deviceBrightness = newBrightness
|
|
||||||
}
|
|
||||||
|
|
||||||
const deviceInfo = getCurrentDeviceInfoByName(actualDevice)
|
|
||||||
|
|
||||||
if (deviceInfo && deviceInfo.class === "ddc") {
|
|
||||||
ddcBrightnessSetProcess.command = ["ddcutil", "setvcp", "-d", String(deviceInfo.ddcDisplay), "10", String(clampedValue)]
|
|
||||||
ddcBrightnessSetProcess.running = true
|
|
||||||
} else {
|
|
||||||
if (device) {
|
|
||||||
brightnessSetProcess.command = ["brightnessctl", "-d", device, "set", `${clampedValue}%`]
|
|
||||||
} else {
|
|
||||||
brightnessSetProcess.command = ["brightnessctl", "set", `${clampedValue}%`]
|
|
||||||
}
|
|
||||||
brightnessSetProcess.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBrightness(percentage, device) {
|
|
||||||
setBrightnessInternal(percentage, device)
|
|
||||||
brightnessChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCurrentDevice(deviceName, saveToSession = false) {
|
|
||||||
if (currentDevice === deviceName) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDevice = deviceName
|
|
||||||
lastIpcDevice = deviceName
|
|
||||||
|
|
||||||
if (saveToSession) {
|
|
||||||
SessionData.setLastBrightnessDevice(deviceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceSwitched()
|
|
||||||
|
|
||||||
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
|
|
||||||
if (deviceInfo && deviceInfo.class === "ddc") {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"]
|
|
||||||
brightnessGetProcess.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshDevices() {
|
|
||||||
deviceListProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshDevicesInternal() {
|
|
||||||
const allDevices = [...devices, ...ddcDevices]
|
|
||||||
|
|
||||||
allDevices.sort((a, b) => {
|
|
||||||
if (a.class === "backlight" && b.class !== "backlight") {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (a.class !== "backlight" && b.class === "backlight") {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.class === "ddc" && b.class !== "ddc" && b.class !== "backlight") {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (a.class !== "ddc" && b.class === "ddc" && a.class !== "backlight") {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
devices = allDevices
|
|
||||||
|
|
||||||
if (devices.length > 0 && !currentDevice) {
|
|
||||||
const lastDevice = SessionData.lastBrightnessDevice || ""
|
|
||||||
const deviceExists = devices.some(d => d.name === lastDevice)
|
|
||||||
if (deviceExists) {
|
|
||||||
setCurrentDevice(lastDevice, false)
|
|
||||||
} else {
|
|
||||||
const nonKbdDevice = devices.find(d => !d.name.includes("kbd")) || devices[0]
|
|
||||||
setCurrentDevice(nonKbdDevice.name, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDeviceBrightness(deviceName) {
|
|
||||||
if (!deviceName) {
|
|
||||||
return
|
|
||||||
} 50
|
|
||||||
|
|
||||||
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
|
|
||||||
if (!deviceInfo) {
|
|
||||||
return 50
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceInfo.class === "ddc") {
|
|
||||||
return deviceBrightness[deviceName] || 50
|
|
||||||
}
|
|
||||||
|
|
||||||
return deviceBrightness[deviceName] || deviceInfo.percentage || 50
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultDevice() {
|
|
||||||
for (const device of devices) {
|
|
||||||
if (device.class === "backlight") {
|
|
||||||
return device.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return devices.length > 0 ? devices[0].name : ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentDeviceInfo() {
|
|
||||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
|
||||||
if (!deviceToUse) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const device of devices) {
|
|
||||||
if (device.name === deviceToUse) {
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCurrentDeviceReady() {
|
|
||||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
|
||||||
if (!deviceToUse) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ddcPendingInit[deviceToUse]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentDeviceInfoByName(deviceName) {
|
|
||||||
if (!deviceName) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const device of devices) {
|
|
||||||
if (device.name === deviceName) {
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function processNextDdcInit() {
|
|
||||||
if (ddcInitQueue.length === 0 || ddcInitialBrightnessProcess.running) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayId = ddcInitQueue.shift()
|
|
||||||
ddcInitialBrightnessProcess.command = ["ddcutil", "getvcp", "-d", String(displayId), "10", "--brief"]
|
|
||||||
ddcInitialBrightnessProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Night Mode Functions - Simplified
|
|
||||||
function enableNightMode() {
|
|
||||||
if (!automationAvailable) {
|
|
||||||
gammaStepTestProcess.running = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nightModeEnabled = true
|
|
||||||
SessionData.setNightModeEnabled(true)
|
|
||||||
|
|
||||||
// Apply immediately or start automation
|
|
||||||
if (SessionData.nightModeAutoEnabled) {
|
|
||||||
startAutomation()
|
|
||||||
} else {
|
|
||||||
applyNightModeDirectly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableNightMode() {
|
|
||||||
nightModeEnabled = false
|
|
||||||
SessionData.setNightModeEnabled(false)
|
|
||||||
stopAutomation()
|
|
||||||
// Nuclear approach - kill ALL gammastep processes multiple times
|
|
||||||
Quickshell.execDetached(["pkill", "-f", "gammastep"])
|
|
||||||
Quickshell.execDetached(["pkill", "-9", "gammastep"])
|
|
||||||
Quickshell.execDetached(["killall", "gammastep"])
|
|
||||||
// Also stop all related processes
|
|
||||||
gammaStepProcess.running = false
|
|
||||||
automationProcess.running = false
|
|
||||||
gammaStepTestProcess.running = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleNightMode() {
|
|
||||||
if (nightModeEnabled) {
|
|
||||||
disableNightMode()
|
|
||||||
} else {
|
|
||||||
enableNightMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyNightModeDirectly() {
|
|
||||||
const temperature = SessionData.nightModeTemperature || 4500
|
|
||||||
gammaStepProcess.command = buildGammastepCommand(["-m", "wayland", "-O", String(temperature)])
|
|
||||||
gammaStepProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetToNormalMode() {
|
|
||||||
// Just kill gammastep to return to normal display temperature
|
|
||||||
Quickshell.execDetached(["pkill", "gammastep"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function startAutomation() {
|
|
||||||
if (!automationAvailable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode = SessionData.nightModeAutoMode || "time"
|
|
||||||
|
|
||||||
switch (mode) {
|
|
||||||
case "time":
|
|
||||||
startTimeBasedMode()
|
|
||||||
break
|
|
||||||
case "location":
|
|
||||||
startLocationBasedMode()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAutomation() {
|
|
||||||
automationProcess.running = false
|
|
||||||
gammaStepProcess.running = false
|
|
||||||
isAutomaticNightTime = false
|
|
||||||
// Nuclear approach - kill ALL gammastep processes multiple times
|
|
||||||
Quickshell.execDetached(["pkill", "-f", "gammastep"])
|
|
||||||
Quickshell.execDetached(["pkill", "-9", "gammastep"])
|
|
||||||
Quickshell.execDetached(["killall", "gammastep"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function startTimeBasedMode() {
|
|
||||||
checkTimeBasedMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
function startLocationBasedMode() {
|
|
||||||
const temperature = SessionData.nightModeTemperature || 4500
|
|
||||||
const dayTemp = 6500
|
|
||||||
|
|
||||||
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
|
||||||
automationProcess.command = buildGammastepCommand(["-m", "wayland", "-l", `${SessionData.latitude.toFixed(6)}:${SessionData.longitude.toFixed(6)}`, "-t", `${dayTemp}:${temperature}`, "-v"])
|
|
||||||
automationProcess.running = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SessionData.nightModeLocationProvider === "geoclue2") {
|
|
||||||
automationProcess.command = buildGammastepCommand(["-m", "wayland", "-l", "geoclue2", "-t", `${dayTemp}:${temperature}`, "-v"])
|
|
||||||
automationProcess.running = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn("DisplayService: Location mode selected but no coordinates or geoclue provider set")
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkTimeBasedMode() {
|
|
||||||
if (!nightModeEnabled || !SessionData.nightModeAutoEnabled || SessionData.nightModeAutoMode !== "time") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTime = systemClock.hours * 60 + systemClock.minutes
|
|
||||||
|
|
||||||
const startMinutes = SessionData.nightModeStartHour * 60 + SessionData.nightModeStartMinute
|
|
||||||
const endMinutes = SessionData.nightModeEndHour * 60 + SessionData.nightModeEndMinute
|
|
||||||
|
|
||||||
let shouldBeNight = false
|
|
||||||
|
|
||||||
if (startMinutes > endMinutes) {
|
|
||||||
shouldBeNight = (currentTime >= startMinutes) || (currentTime < endMinutes)
|
|
||||||
} else {
|
|
||||||
shouldBeNight = (currentTime >= startMinutes) && (currentTime < endMinutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldBeNight !== isAutomaticNightTime) {
|
|
||||||
isAutomaticNightTime = shouldBeNight
|
|
||||||
|
|
||||||
if (shouldBeNight) {
|
|
||||||
applyNightModeDirectly()
|
|
||||||
} else {
|
|
||||||
resetToNormalMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectLocationProviders() {
|
|
||||||
geoclueDetectionProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeAutomationMode(mode) {
|
|
||||||
SessionData.setNightModeAutoMode(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
function evaluateNightMode() {
|
|
||||||
// Always stop all processes first to clean slate
|
|
||||||
stopAutomation()
|
|
||||||
|
|
||||||
if (!nightModeEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SessionData.nightModeAutoEnabled) {
|
|
||||||
restartTimer.nextAction = "automation"
|
|
||||||
restartTimer.start()
|
|
||||||
} else {
|
|
||||||
restartTimer.nextAction = "direct"
|
|
||||||
restartTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkNightModeAvailability() {
|
|
||||||
gammastepAvailabilityProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: restartTimer
|
|
||||||
property string nextAction: ""
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
if (nextAction === "automation") {
|
|
||||||
startAutomation()
|
|
||||||
} else if (nextAction === "direct") {
|
|
||||||
applyNightModeDirectly()
|
|
||||||
}
|
|
||||||
nextAction = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
ddcDetectionProcess.running = true
|
|
||||||
refreshDevices()
|
|
||||||
checkNightModeAvailability()
|
|
||||||
|
|
||||||
// Initialize night mode state from session
|
|
||||||
nightModeEnabled = SessionData.nightModeEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
gammaStepProcess.running = false
|
|
||||||
automationProcess.running = false
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemClock {
|
|
||||||
id: systemClock
|
|
||||||
precision: SystemClock.Minutes
|
|
||||||
onDateChanged: {
|
|
||||||
if (nightModeEnabled && SessionData.nightModeAutoEnabled && SessionData.nightModeAutoMode === "time") {
|
|
||||||
checkTimeBasedMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ddcDetectionProcess
|
|
||||||
|
|
||||||
command: ["which", "ddcutil"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
ddcAvailable = (exitCode === 0)
|
|
||||||
if (ddcAvailable) {
|
|
||||||
ddcDisplayDetectionProcess.running = true
|
|
||||||
} else {
|
|
||||||
console.log("DisplayService: ddcutil not available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ddcDisplayDetectionProcess
|
|
||||||
|
|
||||||
command: ["bash", "-c", "ddcutil detect --brief 2>/dev/null | grep '^Display [0-9]' | awk '{print \"{\\\"display\\\":\" $2 \",\\\"name\\\":\\\"ddc-\" $2 \"\\\",\\\"class\\\":\\\"ddc\\\"}\"}' | tr '\\n' ',' | sed 's/,$//' | sed 's/^/[/' | sed 's/$/]/' || echo '[]'"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (!text.trim()) {
|
|
||||||
ddcDevices = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedDevices = JSON.parse(text.trim())
|
|
||||||
const newDdcDevices = []
|
|
||||||
|
|
||||||
for (const device of parsedDevices) {
|
|
||||||
if (device.display && device.class === "ddc") {
|
|
||||||
newDdcDevices.push({
|
|
||||||
"name": device.name,
|
|
||||||
"class": "ddc",
|
|
||||||
"current": 50,
|
|
||||||
"percentage": 50,
|
|
||||||
"max": 100,
|
|
||||||
"ddcDisplay": device.display
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ddcDevices = newDdcDevices
|
|
||||||
console.log("DisplayService: Found", ddcDevices.length, "DDC displays")
|
|
||||||
|
|
||||||
// Queue initial brightness readings for DDC devices
|
|
||||||
ddcInitQueue = []
|
|
||||||
for (const device of ddcDevices) {
|
|
||||||
ddcInitQueue.push(device.ddcDisplay)
|
|
||||||
// Mark DDC device as pending initialization
|
|
||||||
ddcPendingInit[device.name] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start processing the queue
|
|
||||||
processNextDdcInit()
|
|
||||||
|
|
||||||
// Refresh device list to include DDC devices
|
|
||||||
refreshDevicesInternal()
|
|
||||||
|
|
||||||
// Retry setting last device now that DDC devices are available
|
|
||||||
const lastDevice = SessionData.lastBrightnessDevice || ""
|
|
||||||
if (lastDevice) {
|
|
||||||
const deviceExists = devices.some(d => d.name === lastDevice)
|
|
||||||
if (deviceExists && (!currentDevice || currentDevice !== lastDevice)) {
|
|
||||||
setCurrentDevice(lastDevice, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("DisplayService: Failed to parse DDC devices:", error)
|
|
||||||
ddcDevices = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("DisplayService: Failed to detect DDC displays:", exitCode)
|
|
||||||
ddcDevices = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: deviceListProcess
|
|
||||||
|
|
||||||
command: ["brightnessctl", "-m", "-l"]
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("DisplayService: Failed to list devices:", exitCode)
|
|
||||||
brightnessAvailable = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (!text.trim()) {
|
|
||||||
console.warn("DisplayService: No devices found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const lines = text.trim().split("\n")
|
|
||||||
const newDevices = []
|
|
||||||
for (const line of lines) {
|
|
||||||
const parts = line.split(",")
|
|
||||||
if (parts.length >= 5) {
|
|
||||||
newDevices.push({
|
|
||||||
"name": parts[0],
|
|
||||||
"class": parts[1],
|
|
||||||
"current": parseInt(parts[2]),
|
|
||||||
"percentage": parseInt(parts[3]),
|
|
||||||
"max": parseInt(parts[4])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Store brightnessctl devices separately
|
|
||||||
devices = newDevices
|
|
||||||
|
|
||||||
// Always refresh to combine with DDC devices and set up device selection
|
|
||||||
refreshDevicesInternal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: brightnessSetProcess
|
|
||||||
|
|
||||||
running: false
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("DisplayService: Failed to set brightness:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ddcBrightnessSetProcess
|
|
||||||
|
|
||||||
running: false
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("DisplayService: Failed to set DDC brightness:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ddcInitialBrightnessProcess
|
|
||||||
|
|
||||||
running: false
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("DisplayService: Failed to get initial DDC brightness:", exitCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
processNextDdcInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (!text.trim())
|
|
||||||
return
|
|
||||||
|
|
||||||
const parts = text.trim().split(" ")
|
|
||||||
if (parts.length >= 5) {
|
|
||||||
const current = parseInt(parts[3]) || 50
|
|
||||||
const max = parseInt(parts[4]) || 100
|
|
||||||
const brightness = Math.round((current / max) * 100)
|
|
||||||
|
|
||||||
const commandParts = ddcInitialBrightnessProcess.command
|
|
||||||
if (commandParts && commandParts.length >= 4) {
|
|
||||||
const displayId = commandParts[3]
|
|
||||||
const deviceName = "ddc-" + displayId
|
|
||||||
|
|
||||||
var newBrightness = Object.assign({}, deviceBrightness)
|
|
||||||
newBrightness[deviceName] = brightness
|
|
||||||
deviceBrightness = newBrightness
|
|
||||||
|
|
||||||
var newPending = Object.assign({}, ddcPendingInit)
|
|
||||||
delete newPending[deviceName]
|
|
||||||
ddcPendingInit = newPending
|
|
||||||
|
|
||||||
console.log("DisplayService: Initial DDC Device", deviceName, "brightness:", brightness + "%")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: brightnessGetProcess
|
|
||||||
|
|
||||||
running: false
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("DisplayService: Failed to get brightness:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (!text.trim())
|
|
||||||
return
|
|
||||||
|
|
||||||
const parts = text.trim().split(",")
|
|
||||||
if (parts.length >= 5) {
|
|
||||||
const current = parseInt(parts[2])
|
|
||||||
const max = parseInt(parts[4])
|
|
||||||
maxBrightness = max
|
|
||||||
const brightness = Math.round((current / max) * 100)
|
|
||||||
|
|
||||||
// Update the device brightness cache
|
|
||||||
if (currentDevice) {
|
|
||||||
var newBrightness = Object.assign({}, deviceBrightness)
|
|
||||||
newBrightness[currentDevice] = brightness
|
|
||||||
deviceBrightness = newBrightness
|
|
||||||
}
|
|
||||||
|
|
||||||
brightnessInitialized = true
|
|
||||||
console.log("DisplayService: Device", currentDevice, "brightness:", brightness + "%")
|
|
||||||
brightnessChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ddcBrightnessGetProcess
|
|
||||||
|
|
||||||
running: false
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("DisplayService: Failed to get DDC brightness:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (!text.trim())
|
|
||||||
return
|
|
||||||
|
|
||||||
// Parse ddcutil getvcp output format: "VCP 10 C 50 100"
|
|
||||||
const parts = text.trim().split(" ")
|
|
||||||
if (parts.length >= 5) {
|
|
||||||
const current = parseInt(parts[3]) || 50
|
|
||||||
const max = parseInt(parts[4]) || 100
|
|
||||||
maxBrightness = max
|
|
||||||
const brightness = Math.round((current / max) * 100)
|
|
||||||
|
|
||||||
// Update the device brightness cache
|
|
||||||
if (currentDevice) {
|
|
||||||
var newBrightness = Object.assign({}, deviceBrightness)
|
|
||||||
newBrightness[currentDevice] = brightness
|
|
||||||
deviceBrightness = newBrightness
|
|
||||||
}
|
|
||||||
|
|
||||||
brightnessInitialized = true
|
|
||||||
console.log("DisplayService: DDC Device", currentDevice, "brightness:", brightness + "%")
|
|
||||||
brightnessChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: gammastepAvailabilityProcess
|
|
||||||
command: ["which", "gammastep"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
automationAvailable = (exitCode === 0)
|
|
||||||
if (automationAvailable) {
|
|
||||||
detectLocationProviders()
|
|
||||||
|
|
||||||
// If night mode should be enabled on startup
|
|
||||||
if (nightModeEnabled && SessionData.nightModeAutoEnabled) {
|
|
||||||
startAutomation()
|
|
||||||
} else if (nightModeEnabled) {
|
|
||||||
applyNightModeDirectly()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("DisplayService: gammastep not available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: geoclueDetectionProcess
|
|
||||||
command: ["sh", "-c", "busctl --system list | grep -qF org.freedesktop.GeoClue2"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
geoclueAvailable = (exitCode === 0)
|
|
||||||
console.log("DisplayService: geoclue available:", geoclueAvailable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: gammaStepTestProcess
|
|
||||||
command: ["which", "gammastep"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
automationAvailable = true
|
|
||||||
nightModeEnabled = true
|
|
||||||
SessionData.setNightModeEnabled(true)
|
|
||||||
|
|
||||||
if (SessionData.nightModeAutoEnabled) {
|
|
||||||
startAutomation()
|
|
||||||
} else {
|
|
||||||
applyNightModeDirectly()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("DisplayService: gammastep not found")
|
|
||||||
ToastService.showWarning("Night mode failed: gammastep not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: gammaStepProcess
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (nightModeEnabled && exitCode !== 0 && exitCode !== 15) {
|
|
||||||
console.warn("DisplayService: Night mode process failed:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: automationProcess
|
|
||||||
running: false
|
|
||||||
property string processType: "automation"
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (nightModeEnabled && SessionData.nightModeAutoEnabled && exitCode !== 0 && exitCode !== 15) {
|
|
||||||
console.warn("DisplayService: Night mode automation failed:", exitCode)
|
|
||||||
// Location mode failed
|
|
||||||
console.warn("DisplayService: Location-based night mode failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 onLatitudeChanged() {
|
|
||||||
evaluateNightMode()
|
|
||||||
}
|
|
||||||
function onLongitudeChanged() {
|
|
||||||
evaluateNightMode()
|
|
||||||
}
|
|
||||||
function onNightModeLocationProviderChanged() {
|
|
||||||
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 clampedValue = Math.max(1, Math.min(100, value))
|
|
||||||
const targetDevice = device || ""
|
|
||||||
|
|
||||||
// Ensure device exists if specified
|
|
||||||
if (targetDevice && !root.devices.some(d => d.name === targetDevice)) {
|
|
||||||
return "Device not found: " + targetDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
root.lastIpcDevice = targetDevice
|
|
||||||
if (targetDevice && targetDevice !== root.currentDevice) {
|
|
||||||
root.setCurrentDevice(targetDevice, false)
|
|
||||||
}
|
|
||||||
root.setBrightness(clampedValue, targetDevice)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// Ensure device exists
|
|
||||||
if (actualDevice && !root.devices.some(d => d.name === actualDevice)) {
|
|
||||||
return "Device not found: " + actualDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel
|
|
||||||
const stepValue = parseInt(step || "10")
|
|
||||||
const newLevel = Math.max(1, Math.min(100, currentLevel + stepValue))
|
|
||||||
|
|
||||||
root.lastIpcDevice = targetDevice
|
|
||||||
if (targetDevice && targetDevice !== root.currentDevice) {
|
|
||||||
root.setCurrentDevice(targetDevice, false)
|
|
||||||
}
|
|
||||||
root.setBrightness(newLevel, targetDevice)
|
|
||||||
|
|
||||||
if (targetDevice) {
|
|
||||||
return "Brightness increased to " + newLevel + "% on " + targetDevice
|
|
||||||
} else {
|
|
||||||
return "Brightness increased to " + newLevel + "%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decrement(step: string, device: string): string {
|
|
||||||
if (!root.brightnessAvailable) {
|
|
||||||
return "Brightness control not available"
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetDevice = device || ""
|
|
||||||
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
|
|
||||||
|
|
||||||
// Ensure device exists
|
|
||||||
if (actualDevice && !root.devices.some(d => d.name === actualDevice)) {
|
|
||||||
return "Device not found: " + actualDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel
|
|
||||||
const stepValue = parseInt(step || "10")
|
|
||||||
const newLevel = Math.max(1, Math.min(100, currentLevel - stepValue))
|
|
||||||
|
|
||||||
root.lastIpcDevice = targetDevice
|
|
||||||
if (targetDevice && targetDevice !== root.currentDevice) {
|
|
||||||
root.setCurrentDevice(targetDevice, false)
|
|
||||||
}
|
|
||||||
root.setBrightness(newLevel, targetDevice)
|
|
||||||
|
|
||||||
if (targetDevice) {
|
|
||||||
return "Brightness decreased to " + newLevel + "% on " + targetDevice
|
|
||||||
} else {
|
|
||||||
return "Brightness decreased to " + newLevel + "%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
result += device.name + " (" + device.class + ")\\n"
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
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,60 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
|
||||||
|
|
||||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "mpris"
|
|
||||||
|
|
||||||
function list(): string {
|
|
||||||
return root.availablePlayers.map(p => p.identity).join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
function play(): void {
|
|
||||||
if (root.activePlayer && root.activePlayer.canPlay) {
|
|
||||||
root.activePlayer.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pause(): void {
|
|
||||||
if (root.activePlayer && root.activePlayer.canPause) {
|
|
||||||
root.activePlayer.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function playPause(): void {
|
|
||||||
if (root.activePlayer && root.activePlayer.canTogglePlaying) {
|
|
||||||
root.activePlayer.togglePlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previous(): void {
|
|
||||||
if (root.activePlayer && root.activePlayer.canGoPrevious) {
|
|
||||||
root.activePlayer.previous()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function next(): void {
|
|
||||||
if (root.activePlayer && root.activePlayer.canGoNext) {
|
|
||||||
root.activePlayer.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop(): void {
|
|
||||||
if (root.activePlayer) {
|
|
||||||
root.activePlayer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,666 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var workspaces: ({})
|
|
||||||
property var allWorkspaces: []
|
|
||||||
property int focusedWorkspaceIndex: 0
|
|
||||||
property string focusedWorkspaceId: ""
|
|
||||||
property var currentOutputWorkspaces: []
|
|
||||||
property string currentOutput: ""
|
|
||||||
|
|
||||||
property var outputs: ({})
|
|
||||||
|
|
||||||
property var windows: []
|
|
||||||
|
|
||||||
property bool inOverview: false
|
|
||||||
|
|
||||||
property int currentKeyboardLayoutIndex: 0
|
|
||||||
property var keyboardLayoutNames: []
|
|
||||||
|
|
||||||
property string configValidationOutput: ""
|
|
||||||
property bool hasInitialConnection: false
|
|
||||||
property bool suppressConfigToast: true
|
|
||||||
property bool suppressNextConfigToast: false
|
|
||||||
property bool matugenSuppression: false
|
|
||||||
|
|
||||||
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
fetchOutputs()
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchOutputs() {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
outputsProcess.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: outputsProcess
|
|
||||||
command: ["niri", "msg", "-j", "outputs"]
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
try {
|
|
||||||
const outputsData = JSON.parse(text)
|
|
||||||
outputs = outputsData
|
|
||||||
console.log("NiriService: Loaded", Object.keys(outputsData).length, "outputs")
|
|
||||||
if (windows.length > 0) {
|
|
||||||
windows = sortWindowsByLayout(windows)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("NiriService: Failed to parse outputs:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("NiriService: Failed to fetch outputs, exit code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Socket {
|
|
||||||
id: eventStreamSocket
|
|
||||||
path: root.socketPath
|
|
||||||
connected: CompositorService.isNiri
|
|
||||||
|
|
||||||
onConnectionStateChanged: {
|
|
||||||
if (connected) {
|
|
||||||
write('"EventStream"\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser: SplitParser {
|
|
||||||
onRead: line => {
|
|
||||||
try {
|
|
||||||
const event = JSON.parse(line)
|
|
||||||
handleNiriEvent(event)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("NiriService: Failed to parse event:", line, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Socket {
|
|
||||||
id: requestSocket
|
|
||||||
path: root.socketPath
|
|
||||||
connected: CompositorService.isNiri
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortWindowsByLayout(windowList) {
|
|
||||||
return [...windowList].sort((a, b) => {
|
|
||||||
const aWorkspace = workspaces[a.workspace_id]
|
|
||||||
const bWorkspace = workspaces[b.workspace_id]
|
|
||||||
|
|
||||||
if (aWorkspace && bWorkspace) {
|
|
||||||
const aOutput = aWorkspace.output
|
|
||||||
const bOutput = bWorkspace.output
|
|
||||||
|
|
||||||
const aOutputInfo = outputs[aOutput]
|
|
||||||
const bOutputInfo = outputs[bOutput]
|
|
||||||
|
|
||||||
if (aOutputInfo && bOutputInfo && aOutputInfo.logical && bOutputInfo.logical) {
|
|
||||||
if (aOutputInfo.logical.x !== bOutputInfo.logical.x) {
|
|
||||||
return aOutputInfo.logical.x - bOutputInfo.logical.x
|
|
||||||
}
|
|
||||||
if (aOutputInfo.logical.y !== bOutputInfo.logical.y) {
|
|
||||||
return aOutputInfo.logical.y - bOutputInfo.logical.y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aOutput === bOutput && aWorkspace.idx !== bWorkspace.idx) {
|
|
||||||
return aWorkspace.idx - bWorkspace.idx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.workspace_id === b.workspace_id && a.layout && b.layout) {
|
|
||||||
|
|
||||||
if (a.layout.pos_in_scrolling_layout && b.layout.pos_in_scrolling_layout) {
|
|
||||||
const aPos = a.layout.pos_in_scrolling_layout
|
|
||||||
const bPos = b.layout.pos_in_scrolling_layout
|
|
||||||
|
|
||||||
if (aPos.length > 1 && bPos.length > 1) {
|
|
||||||
if (aPos[0] !== bPos[0]) {
|
|
||||||
return aPos[0] - bPos[0]
|
|
||||||
}
|
|
||||||
if (aPos[1] !== bPos[1]) {
|
|
||||||
return aPos[1] - bPos[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.id - b.id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNiriEvent(event) {
|
|
||||||
const eventType = Object.keys(event)[0];
|
|
||||||
|
|
||||||
switch (eventType) {
|
|
||||||
case 'WorkspacesChanged':
|
|
||||||
handleWorkspacesChanged(event.WorkspacesChanged);
|
|
||||||
break;
|
|
||||||
case 'WorkspaceActivated':
|
|
||||||
handleWorkspaceActivated(event.WorkspaceActivated);
|
|
||||||
break;
|
|
||||||
case 'WorkspaceActiveWindowChanged':
|
|
||||||
handleWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged);
|
|
||||||
break;
|
|
||||||
case 'WindowsChanged':
|
|
||||||
handleWindowsChanged(event.WindowsChanged);
|
|
||||||
break;
|
|
||||||
case 'WindowClosed':
|
|
||||||
handleWindowClosed(event.WindowClosed);
|
|
||||||
break;
|
|
||||||
case 'WindowOpenedOrChanged':
|
|
||||||
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
|
|
||||||
break;
|
|
||||||
case 'WindowLayoutsChanged':
|
|
||||||
handleWindowLayoutsChanged(event.WindowLayoutsChanged);
|
|
||||||
break;
|
|
||||||
case 'OutputsChanged':
|
|
||||||
handleOutputsChanged(event.OutputsChanged);
|
|
||||||
break;
|
|
||||||
case 'OverviewOpenedOrClosed':
|
|
||||||
handleOverviewChanged(event.OverviewOpenedOrClosed);
|
|
||||||
break;
|
|
||||||
case 'ConfigLoaded':
|
|
||||||
handleConfigLoaded(event.ConfigLoaded);
|
|
||||||
break;
|
|
||||||
case 'KeyboardLayoutsChanged':
|
|
||||||
handleKeyboardLayoutsChanged(event.KeyboardLayoutsChanged);
|
|
||||||
break;
|
|
||||||
case 'KeyboardLayoutSwitched':
|
|
||||||
handleKeyboardLayoutSwitched(event.KeyboardLayoutSwitched);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWorkspacesChanged(data) {
|
|
||||||
const workspaces = {}
|
|
||||||
|
|
||||||
for (const ws of data.workspaces) {
|
|
||||||
workspaces[ws.id] = ws
|
|
||||||
}
|
|
||||||
|
|
||||||
root.workspaces = workspaces
|
|
||||||
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx)
|
|
||||||
|
|
||||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
|
|
||||||
if (focusedWorkspaceIndex >= 0) {
|
|
||||||
const focusedWs = allWorkspaces[focusedWorkspaceIndex]
|
|
||||||
focusedWorkspaceId = focusedWs.id
|
|
||||||
currentOutput = focusedWs.output || ""
|
|
||||||
} else {
|
|
||||||
focusedWorkspaceIndex = 0
|
|
||||||
focusedWorkspaceId = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentOutputWorkspaces()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWorkspaceActivated(data) {
|
|
||||||
const ws = root.workspaces[data.id]
|
|
||||||
if (!ws) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const output = ws.output
|
|
||||||
|
|
||||||
for (const id in root.workspaces) {
|
|
||||||
const workspace = root.workspaces[id]
|
|
||||||
const got_activated = workspace.id === data.id
|
|
||||||
|
|
||||||
if (workspace.output === output) {
|
|
||||||
workspace.is_active = got_activated
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.focused) {
|
|
||||||
workspace.is_focused = got_activated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
focusedWorkspaceId = data.id
|
|
||||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id)
|
|
||||||
|
|
||||||
if (focusedWorkspaceIndex >= 0) {
|
|
||||||
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx)
|
|
||||||
|
|
||||||
updateCurrentOutputWorkspaces()
|
|
||||||
workspacesChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWorkspaceActiveWindowChanged(data) {
|
|
||||||
if (data.active_window_id !== null && data.active_window_id !== undefined) {
|
|
||||||
const updatedWindows = []
|
|
||||||
for (var i = 0; i < windows.length; i++) {
|
|
||||||
const w = windows[i]
|
|
||||||
const updatedWindow = {}
|
|
||||||
for (let prop in w) {
|
|
||||||
updatedWindow[prop] = w[prop]
|
|
||||||
}
|
|
||||||
updatedWindow.is_focused = (w.id == data.active_window_id)
|
|
||||||
updatedWindows.push(updatedWindow)
|
|
||||||
}
|
|
||||||
windows = updatedWindows
|
|
||||||
} else {
|
|
||||||
const updatedWindows = []
|
|
||||||
for (var i = 0; i < windows.length; i++) {
|
|
||||||
const w = windows[i]
|
|
||||||
const updatedWindow = {}
|
|
||||||
for (let prop in w) {
|
|
||||||
updatedWindow[prop] = w[prop]
|
|
||||||
}
|
|
||||||
updatedWindow.is_focused = w.workspace_id == data.workspace_id ? false : w.is_focused
|
|
||||||
updatedWindows.push(updatedWindow)
|
|
||||||
}
|
|
||||||
windows = updatedWindows
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWindowsChanged(data) {
|
|
||||||
windows = sortWindowsByLayout(data.windows)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWindowClosed(data) {
|
|
||||||
windows = windows.filter(w => w.id !== data.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWindowOpenedOrChanged(data) {
|
|
||||||
if (!data.window) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const window = data.window
|
|
||||||
const existingIndex = windows.findIndex(w => w.id === window.id)
|
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
const updatedWindows = [...windows]
|
|
||||||
updatedWindows[existingIndex] = window
|
|
||||||
windows = sortWindowsByLayout(updatedWindows)
|
|
||||||
} else {
|
|
||||||
windows = sortWindowsByLayout([...windows, window])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWindowLayoutsChanged(data) {
|
|
||||||
if (!data.changes) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedWindows = [...windows]
|
|
||||||
let hasChanges = false
|
|
||||||
|
|
||||||
for (const change of data.changes) {
|
|
||||||
const windowId = change[0]
|
|
||||||
const layoutData = change[1]
|
|
||||||
|
|
||||||
const windowIndex = updatedWindows.findIndex(w => w.id === windowId)
|
|
||||||
if (windowIndex >= 0) {
|
|
||||||
const updatedWindow = {}
|
|
||||||
for (var prop in updatedWindows[windowIndex]) {
|
|
||||||
updatedWindow[prop] = updatedWindows[windowIndex][prop]
|
|
||||||
}
|
|
||||||
updatedWindow.layout = layoutData
|
|
||||||
updatedWindows[windowIndex] = updatedWindow
|
|
||||||
hasChanges = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChanges) {
|
|
||||||
windows = sortWindowsByLayout(updatedWindows)
|
|
||||||
windowsChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOutputsChanged(data) {
|
|
||||||
if (data.outputs) {
|
|
||||||
outputs = data.outputs
|
|
||||||
windows = sortWindowsByLayout(windows)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOverviewChanged(data) {
|
|
||||||
inOverview = data.is_open
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleConfigLoaded(data) {
|
|
||||||
if (data.failed) {
|
|
||||||
validateProcess.running = true
|
|
||||||
} else {
|
|
||||||
configValidationOutput = ""
|
|
||||||
if (ToastService.toastVisible && ToastService.currentLevel === ToastService.levelError) {
|
|
||||||
ToastService.hideToast()
|
|
||||||
}
|
|
||||||
if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) {
|
|
||||||
ToastService.showInfo("niri: config reloaded")
|
|
||||||
} else if (suppressNextConfigToast) {
|
|
||||||
suppressNextConfigToast = false
|
|
||||||
suppressResetTimer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasInitialConnection) {
|
|
||||||
hasInitialConnection = true
|
|
||||||
suppressToastTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyboardLayoutsChanged(data) {
|
|
||||||
keyboardLayoutNames = data.keyboard_layouts.names
|
|
||||||
currentKeyboardLayoutIndex = data.keyboard_layouts.current_idx
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyboardLayoutSwitched(data) {
|
|
||||||
currentKeyboardLayoutIndex = data.idx
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: validateProcess
|
|
||||||
command: ["niri", "validate"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const lines = text.split('\n')
|
|
||||||
const trimmedLines = lines.map(line => line.replace(/\s+$/, '')).filter(line => line.length > 0)
|
|
||||||
configValidationOutput = trimmedLines.join('\n').trim()
|
|
||||||
if (hasInitialConnection) {
|
|
||||||
ToastService.showError("niri: failed to load config", configValidationOutput)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
configValidationOutput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCurrentOutputWorkspaces() {
|
|
||||||
if (!currentOutput) {
|
|
||||||
currentOutputWorkspaces = allWorkspaces
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputWs = allWorkspaces.filter(w => w.output === currentOutput)
|
|
||||||
currentOutputWorkspaces = outputWs
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(request) {
|
|
||||||
if (!CompositorService.isNiri || !requestSocket.connected) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
requestSocket.write(JSON.stringify(request) + "\n")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function doScreenTransition() {
|
|
||||||
return send({
|
|
||||||
"Action": {
|
|
||||||
"DoScreenTransition": {
|
|
||||||
"delay_ms": 100,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchToWorkspace(workspaceIndex) {
|
|
||||||
return send({
|
|
||||||
"Action": {
|
|
||||||
"FocusWorkspace": {
|
|
||||||
"reference": {
|
|
||||||
"Index": workspaceIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusWindow(windowId) {
|
|
||||||
return send({
|
|
||||||
"Action": {
|
|
||||||
"FocusWindow": {
|
|
||||||
"id": windowId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function powerOffMonitors() {
|
|
||||||
return send({
|
|
||||||
"Action": {
|
|
||||||
"PowerOffMonitors": {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function powerOnMonitors() {
|
|
||||||
return send({
|
|
||||||
"Action": {
|
|
||||||
"PowerOnMonitors": {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentOutputWorkspaceNumbers() {
|
|
||||||
return currentOutputWorkspaces.map(w => w.idx + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentWorkspaceNumber() {
|
|
||||||
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
|
|
||||||
return allWorkspaces[focusedWorkspaceIndex].idx + 1
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentKeyboardLayoutName() {
|
|
||||||
if (currentKeyboardLayoutIndex >= 0 && currentKeyboardLayoutIndex < keyboardLayoutNames.length) {
|
|
||||||
return keyboardLayoutNames[currentKeyboardLayoutIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function cycleKeyboardLayout() {
|
|
||||||
return send({
|
|
||||||
"Action": {
|
|
||||||
"SwitchLayout": {
|
|
||||||
"layout": "Next"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function quit() {
|
|
||||||
return send({
|
|
||||||
"Action": {
|
|
||||||
"Quit": {
|
|
||||||
"skip_confirmation": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function suppressNextToast() {
|
|
||||||
matugenSuppression = true
|
|
||||||
suppressResetTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNiriWindow(toplevel) {
|
|
||||||
if (!toplevel.appId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var j = 0; j < windows.length; j++) {
|
|
||||||
const niriWindow = windows[j]
|
|
||||||
if (niriWindow.app_id === toplevel.appId) {
|
|
||||||
if (!niriWindow.title || niriWindow.title === toplevel.title) {
|
|
||||||
return {
|
|
||||||
"niriIndex": j,
|
|
||||||
"niriWindow": niriWindow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortToplevels(toplevels) {
|
|
||||||
if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) {
|
|
||||||
return [...toplevels]
|
|
||||||
}
|
|
||||||
|
|
||||||
const usedToplevels = new Set()
|
|
||||||
const enrichedToplevels = []
|
|
||||||
|
|
||||||
for (const niriWindow of sortWindowsByLayout(windows)) {
|
|
||||||
let bestMatch = null
|
|
||||||
|
|
||||||
for (const toplevel of toplevels) {
|
|
||||||
if (usedToplevels.has(toplevel)) continue
|
|
||||||
|
|
||||||
if (toplevel.appId === niriWindow.app_id) {
|
|
||||||
if (niriWindow.title && toplevel.title === niriWindow.title) {
|
|
||||||
bestMatch = toplevel
|
|
||||||
break
|
|
||||||
} else if (!niriWindow.title && !bestMatch) {
|
|
||||||
bestMatch = toplevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestMatch) {
|
|
||||||
usedToplevels.add(bestMatch)
|
|
||||||
|
|
||||||
const enrichedToplevel = {
|
|
||||||
appId: bestMatch.appId,
|
|
||||||
title: bestMatch.title,
|
|
||||||
activated: bestMatch.activated,
|
|
||||||
niriWindowId: niriWindow.id,
|
|
||||||
niriWorkspaceId: niriWindow.workspace_id,
|
|
||||||
activate: function() {
|
|
||||||
return NiriService.focusWindow(niriWindow.id)
|
|
||||||
},
|
|
||||||
close: function() {
|
|
||||||
if (bestMatch.close) {
|
|
||||||
return bestMatch.close()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let prop in bestMatch) {
|
|
||||||
if (!(prop in enrichedToplevel)) {
|
|
||||||
enrichedToplevel[prop] = bestMatch[prop]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enrichedToplevels.push(enrichedToplevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const toplevel of toplevels) {
|
|
||||||
if (!usedToplevels.has(toplevel)) {
|
|
||||||
enrichedToplevels.push(toplevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return enrichedToplevels
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterCurrentWorkspace(toplevels, screenName) {
|
|
||||||
let currentWorkspaceId = null
|
|
||||||
for (var i = 0; i < allWorkspaces.length; i++) {
|
|
||||||
const ws = allWorkspaces[i]
|
|
||||||
if (ws.output === screenName && ws.is_active) {
|
|
||||||
currentWorkspaceId = ws.id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentWorkspaceId === null) {
|
|
||||||
return toplevels
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceWindows = windows.filter(niriWindow => niriWindow.workspace_id === currentWorkspaceId)
|
|
||||||
const usedToplevels = new Set()
|
|
||||||
const result = []
|
|
||||||
|
|
||||||
for (const niriWindow of workspaceWindows) {
|
|
||||||
let bestMatch = null
|
|
||||||
|
|
||||||
for (const toplevel of toplevels) {
|
|
||||||
if (usedToplevels.has(toplevel)) continue
|
|
||||||
|
|
||||||
if (toplevel.appId === niriWindow.app_id) {
|
|
||||||
if (niriWindow.title && toplevel.title === niriWindow.title) {
|
|
||||||
bestMatch = toplevel
|
|
||||||
break
|
|
||||||
} else if (!niriWindow.title && !bestMatch) {
|
|
||||||
bestMatch = toplevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestMatch) {
|
|
||||||
usedToplevels.add(bestMatch)
|
|
||||||
|
|
||||||
const enrichedToplevel = {
|
|
||||||
appId: bestMatch.appId,
|
|
||||||
title: bestMatch.title,
|
|
||||||
activated: bestMatch.activated,
|
|
||||||
niriWindowId: niriWindow.id,
|
|
||||||
niriWorkspaceId: niriWindow.workspace_id,
|
|
||||||
activate: function() {
|
|
||||||
return NiriService.focusWindow(niriWindow.id)
|
|
||||||
},
|
|
||||||
close: function() {
|
|
||||||
if (bestMatch.close) {
|
|
||||||
return bestMatch.close()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let prop in bestMatch) {
|
|
||||||
if (!(prop in enrichedToplevel)) {
|
|
||||||
enrichedToplevel[prop] = bestMatch[prop]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(enrichedToplevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: suppressToastTimer
|
|
||||||
interval: 3000
|
|
||||||
onTriggered: root.suppressConfigToast = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: suppressResetTimer
|
|
||||||
interval: 2000
|
|
||||||
onTriggered: root.matugenSuppression = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool accountsServiceAvailable: false
|
|
||||||
property string systemProfileImage: ""
|
|
||||||
property string profileImage: ""
|
|
||||||
property bool settingsPortalAvailable: false
|
|
||||||
property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light
|
|
||||||
|
|
||||||
function init() {}
|
|
||||||
|
|
||||||
function getSystemProfileImage() {
|
|
||||||
systemProfileCheckProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function setProfileImage(imagePath) {
|
|
||||||
profileImage = imagePath
|
|
||||||
if (accountsServiceAvailable && imagePath) {
|
|
||||||
setSystemProfileImage(imagePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemColorScheme() {
|
|
||||||
systemColorSchemeCheckProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLightMode(isLightMode) {
|
|
||||||
if (settingsPortalAvailable) {
|
|
||||||
setSystemColorScheme(isLightMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSystemColorScheme(isLightMode) {
|
|
||||||
if (!settingsPortalAvailable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorScheme = isLightMode ? "prefer-light" : "prefer-dark"
|
|
||||||
const script = `gsettings set org.gnome.desktop.interface color-scheme '${colorScheme}'`
|
|
||||||
|
|
||||||
systemColorSchemeSetProcess.command = ["bash", "-c", script]
|
|
||||||
systemColorSchemeSetProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSystemProfileImage(imagePath) {
|
|
||||||
if (!accountsServiceAvailable || !imagePath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${imagePath}'`
|
|
||||||
|
|
||||||
systemProfileSetProcess.command = ["bash", "-c", script]
|
|
||||||
systemProfileSetProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
checkAccountsService()
|
|
||||||
checkSettingsPortal()
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkAccountsService() {
|
|
||||||
accountsServiceCheckProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkSettingsPortal() {
|
|
||||||
settingsPortalCheckProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: accountsServiceCheckProcess
|
|
||||||
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts org.freedesktop.Accounts.FindUserByName string:\"$USER\""]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
root.accountsServiceAvailable = (exitCode === 0)
|
|
||||||
if (root.accountsServiceAvailable) {
|
|
||||||
root.getSystemProfileImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: systemProfileCheckProcess
|
|
||||||
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const match = text.match(/string\s+"([^"]+)"/)
|
|
||||||
if (match && match[1] && match[1] !== "" && match[1] !== "/var/lib/AccountsService/icons/") {
|
|
||||||
root.systemProfileImage = match[1]
|
|
||||||
|
|
||||||
if (!root.profileImage || root.profileImage === "") {
|
|
||||||
root.profileImage = root.systemProfileImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.systemProfileImage = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: systemProfileSetProcess
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
root.getSystemProfileImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: settingsPortalCheckProcess
|
|
||||||
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
root.settingsPortalAvailable = (exitCode === 0)
|
|
||||||
if (root.settingsPortalAvailable) {
|
|
||||||
root.getSystemColorScheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: systemColorSchemeCheckProcess
|
|
||||||
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const match = text.match(/uint32 (\d+)/)
|
|
||||||
if (match && match[1]) {
|
|
||||||
root.systemColorScheme = parseInt(match[1])
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
const shouldBeLightMode = (root.systemColorScheme === 2)
|
|
||||||
if (Theme.isLightMode !== shouldBeLightMode) {
|
|
||||||
Theme.isLightMode = shouldBeLightMode
|
|
||||||
if (typeof SessionData !== "undefined") {
|
|
||||||
SessionData.setLightMode(shouldBeLightMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.systemColorScheme = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: systemColorSchemeSetProcess
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
root.getSystemColorScheme()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "profile"
|
|
||||||
|
|
||||||
function getImage(): string {
|
|
||||||
return root.profileImage
|
|
||||||
}
|
|
||||||
|
|
||||||
function setImage(path: string): string {
|
|
||||||
if (!path) {
|
|
||||||
return "ERROR: No path provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = path.startsWith("/") ? path : `${StandardPaths.writableLocation(StandardPaths.HomeLocation)}/${path}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
root.setProfileImage(absolutePath)
|
|
||||||
return "SUCCESS: Profile image set to " + absolutePath
|
|
||||||
} catch (e) {
|
|
||||||
return "ERROR: Failed to set profile image: " + e.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearImage(): string {
|
|
||||||
root.setProfileImage("")
|
|
||||||
return "SUCCESS: Profile image cleared"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool hasUwsm: false
|
|
||||||
property bool isElogind: false
|
|
||||||
property bool hibernateSupported: false
|
|
||||||
property bool inhibitorAvailable: true
|
|
||||||
property bool idleInhibited: false
|
|
||||||
property string inhibitReason: "Keep system awake"
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
detectElogindProcess.running = true
|
|
||||||
detectHibernateProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: detectUwsmProcess
|
|
||||||
running: false
|
|
||||||
command: ["which", "uwsm"]
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
hasUwsm = (exitCode === 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: detectElogindProcess
|
|
||||||
running: false
|
|
||||||
command: ["sh", "-c", "ps -eo comm= | grep -E '^(elogind|elogind-daemon)$'"]
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
console.log("SessionService: Elogind detection exited with code", exitCode)
|
|
||||||
isElogind = (exitCode === 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: detectHibernateProcess
|
|
||||||
running: false
|
|
||||||
command: ["grep", "-q", "disk", "/sys/power/state"]
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
hibernateSupported = (exitCode === 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: uwsmLogout
|
|
||||||
command: ["uwsm", "stop"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
onRead: data => {
|
|
||||||
if (data.trim().toLowerCase().includes("not running")) {
|
|
||||||
_logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// * Apps
|
|
||||||
function launchDesktopEntry(desktopEntry) {
|
|
||||||
let cmd = desktopEntry.command
|
|
||||||
if (SessionData.launchPrefix && SessionData.launchPrefix.length > 0) {
|
|
||||||
const launchPrefix = SessionData.launchPrefix.trim().split(" ")
|
|
||||||
cmd = launchPrefix.concat(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
Quickshell.execDetached({
|
|
||||||
command: cmd,
|
|
||||||
workingDirectory: desktopEntry.workingDirectory,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// * Session management
|
|
||||||
function logout() {
|
|
||||||
if (hasUwsm) {
|
|
||||||
uwsmLogout.running = true
|
|
||||||
}
|
|
||||||
_logout()
|
|
||||||
}
|
|
||||||
|
|
||||||
function _logout() {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
NiriService.quit()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hyprland fallback
|
|
||||||
Hyprland.dispatch("exit")
|
|
||||||
}
|
|
||||||
|
|
||||||
function suspend() {
|
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function hibernate() {
|
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "hibernate"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function reboot() {
|
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function poweroff() {
|
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// * Idle Inhibitor
|
|
||||||
signal inhibitorChanged
|
|
||||||
|
|
||||||
function enableIdleInhibit() {
|
|
||||||
if (idleInhibited) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
idleInhibited = true
|
|
||||||
inhibitorChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableIdleInhibit() {
|
|
||||||
if (!idleInhibited) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
idleInhibited = false
|
|
||||||
inhibitorChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleIdleInhibit() {
|
|
||||||
if (idleInhibited) {
|
|
||||||
disableIdleInhibit()
|
|
||||||
} else {
|
|
||||||
enableIdleInhibit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setInhibitReason(reason) {
|
|
||||||
inhibitReason = reason
|
|
||||||
|
|
||||||
if (idleInhibited) {
|
|
||||||
const wasActive = idleInhibited
|
|
||||||
idleInhibited = false
|
|
||||||
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (wasActive) {
|
|
||||||
idleInhibited = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: idleInhibitProcess
|
|
||||||
|
|
||||||
command: {
|
|
||||||
if (!idleInhibited) {
|
|
||||||
return ["true"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", `--why=${inhibitReason}`, "--mode=block", "sleep", "infinity"]
|
|
||||||
}
|
|
||||||
|
|
||||||
running: idleInhibited
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (idleInhibited && exitCode !== 0) {
|
|
||||||
console.warn("SessionService: Inhibitor process crashed with exit code:", exitCode)
|
|
||||||
idleInhibited = false
|
|
||||||
ToastService.showWarning("Idle inhibitor failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function toggle(): string {
|
|
||||||
root.toggleIdleInhibit()
|
|
||||||
return root.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function enable(): string {
|
|
||||||
root.enableIdleInhibit()
|
|
||||||
return "Idle inhibit enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function disable(): string {
|
|
||||||
root.disableIdleInhibit()
|
|
||||||
return "Idle inhibit disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function status(): string {
|
|
||||||
return root.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function reason(newReason: string): string {
|
|
||||||
if (!newReason) {
|
|
||||||
return `Current reason: ${root.inhibitReason}`
|
|
||||||
}
|
|
||||||
|
|
||||||
root.setInhibitReason(newReason)
|
|
||||||
return `Inhibit reason set to: ${newReason}`
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "inhibit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var availableUpdates: []
|
|
||||||
property bool isChecking: false
|
|
||||||
property bool hasError: false
|
|
||||||
property string errorMessage: ""
|
|
||||||
property string pkgManager: ""
|
|
||||||
property string distribution: ""
|
|
||||||
property bool distributionSupported: false
|
|
||||||
|
|
||||||
readonly property list<string> supportedDistributions: ["arch", "cachyos", "manjaro", "endeavouros"]
|
|
||||||
readonly property int updateCount: availableUpdates.length
|
|
||||||
readonly property bool helperAvailable: pkgManager !== "" && distributionSupported
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: distributionDetection
|
|
||||||
command: ["sh", "-c", "cat /etc/os-release | grep '^ID=' | cut -d'=' -f2 | tr -d '\"'"]
|
|
||||||
running: true
|
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
distribution = stdout.text.trim().toLowerCase()
|
|
||||||
distributionSupported = supportedDistributions.includes(distribution)
|
|
||||||
|
|
||||||
if (distributionSupported) {
|
|
||||||
helperDetection.running = true
|
|
||||||
} else {
|
|
||||||
console.warn("SystemUpdate: Unsupported distribution:", distribution)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("SystemUpdate: Failed to detect distribution")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: helperDetection
|
|
||||||
command: ["sh", "-c", "which paru || which yay"]
|
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
const helperPath = stdout.text.trim()
|
|
||||||
pkgManager = helperPath.split('/').pop()
|
|
||||||
checkForUpdates()
|
|
||||||
} else {
|
|
||||||
console.warn("SystemUpdate: No package manager found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: updateChecker
|
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
isChecking = false
|
|
||||||
if (exitCode === 0 || exitCode === 1) {
|
|
||||||
// Exit code 0 = updates available, 1 = no updates
|
|
||||||
parseUpdates(stdout.text)
|
|
||||||
hasError = false
|
|
||||||
errorMessage = ""
|
|
||||||
} else {
|
|
||||||
hasError = true
|
|
||||||
errorMessage = "Failed to check for updates"
|
|
||||||
console.warn("SystemUpdate: Update check failed with code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: updater
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
checkForUpdates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForUpdates() {
|
|
||||||
if (!distributionSupported || !pkgManager || isChecking) return
|
|
||||||
|
|
||||||
isChecking = true
|
|
||||||
hasError = false
|
|
||||||
updateChecker.command = [pkgManager, "-Qu"]
|
|
||||||
updateChecker.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseUpdates(output) {
|
|
||||||
const lines = output.trim().split('\n').filter(line => line.trim())
|
|
||||||
const updates = []
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const match = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
|
|
||||||
if (match) {
|
|
||||||
updates.push({
|
|
||||||
name: match[1],
|
|
||||||
currentVersion: match[2],
|
|
||||||
newVersion: match[3],
|
|
||||||
description: `${match[1]} ${match[2]} → ${match[3]}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
availableUpdates = updates
|
|
||||||
}
|
|
||||||
|
|
||||||
function runUpdates() {
|
|
||||||
if (!distributionSupported || !pkgManager || updateCount === 0) return
|
|
||||||
|
|
||||||
const terminal = Quickshell.env("TERMINAL") || "xterm"
|
|
||||||
const updateCommand = `${pkgManager} -Syu && echo "Updates complete! Press Enter to close..." && read`
|
|
||||||
|
|
||||||
updater.command = [terminal, "-e", "sh", "-c", updateCommand]
|
|
||||||
updater.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 30 * 60 * 1000
|
|
||||||
repeat: true
|
|
||||||
running: distributionSupported && pkgManager
|
|
||||||
onTriggered: checkForUpdates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property int levelInfo: 0
|
|
||||||
readonly property int levelWarn: 1
|
|
||||||
readonly property int levelError: 2
|
|
||||||
property string currentMessage: ""
|
|
||||||
property int currentLevel: levelInfo
|
|
||||||
property bool toastVisible: false
|
|
||||||
property var toastQueue: []
|
|
||||||
property string currentDetails: ""
|
|
||||||
property bool hasDetails: false
|
|
||||||
property string wallpaperErrorStatus: ""
|
|
||||||
|
|
||||||
function showToast(message, level = levelInfo, details = "") {
|
|
||||||
toastQueue.push({
|
|
||||||
"message": message,
|
|
||||||
"level": level,
|
|
||||||
"details": details
|
|
||||||
})
|
|
||||||
if (!toastVisible) {
|
|
||||||
processQueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showInfo(message, details = "") {
|
|
||||||
showToast(message, levelInfo, details)
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWarning(message, details = "") {
|
|
||||||
showToast(message, levelWarn, details)
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message, details = "") {
|
|
||||||
showToast(message, levelError, details)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideToast() {
|
|
||||||
toastVisible = false
|
|
||||||
currentMessage = ""
|
|
||||||
currentDetails = ""
|
|
||||||
hasDetails = false
|
|
||||||
currentLevel = levelInfo
|
|
||||||
toastTimer.stop()
|
|
||||||
resetToastState()
|
|
||||||
if (toastQueue.length > 0) {
|
|
||||||
processQueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processQueue() {
|
|
||||||
if (toastQueue.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const toast = toastQueue.shift()
|
|
||||||
currentMessage = toast.message
|
|
||||||
currentLevel = toast.level
|
|
||||||
currentDetails = toast.details || ""
|
|
||||||
hasDetails = currentDetails.length > 0
|
|
||||||
toastVisible = true
|
|
||||||
resetToastState()
|
|
||||||
|
|
||||||
if (toast.level === levelError && hasDetails) {
|
|
||||||
toastTimer.interval = 8000
|
|
||||||
toastTimer.start()
|
|
||||||
} else {
|
|
||||||
toastTimer.interval = toast.level === levelError ? 5000 : toast.level === levelWarn ? 3000 : 1500
|
|
||||||
toastTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signal resetToastState
|
|
||||||
|
|
||||||
function stopTimer() {
|
|
||||||
toastTimer.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
function restartTimer() {
|
|
||||||
if (hasDetails && currentLevel === levelError) {
|
|
||||||
toastTimer.interval = 8000
|
|
||||||
toastTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearWallpaperError() {
|
|
||||||
wallpaperErrorStatus = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: toastTimer
|
|
||||||
|
|
||||||
interval: 5000
|
|
||||||
running: false
|
|
||||||
repeat: false
|
|
||||||
onTriggered: hideToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string username: ""
|
|
||||||
property string fullName: ""
|
|
||||||
property string profilePicture: ""
|
|
||||||
property string uptime: ""
|
|
||||||
property string shortUptime: ""
|
|
||||||
property string hostname: ""
|
|
||||||
property bool profileAvailable: false
|
|
||||||
|
|
||||||
function getUserInfo() {
|
|
||||||
userInfoProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUptime() {
|
|
||||||
uptimeProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshUserInfo() {
|
|
||||||
getUserInfo()
|
|
||||||
getUptime()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
getUserInfo()
|
|
||||||
getUptime()
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: userInfoProcess
|
|
||||||
|
|
||||||
command: ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""]
|
|
||||||
running: false
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
|
|
||||||
root.username = "User"
|
|
||||||
root.fullName = "User"
|
|
||||||
root.hostname = "System"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const parts = text.trim().split("|")
|
|
||||||
if (parts.length >= 3) {
|
|
||||||
root.username = parts[0] || ""
|
|
||||||
root.fullName = parts[1] || parts[0] || ""
|
|
||||||
root.hostname = parts[2] || ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: uptimeProcess
|
|
||||||
|
|
||||||
command: ["cat", "/proc/uptime"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.uptime = "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const seconds = parseInt(text.split(" ")[0])
|
|
||||||
const days = Math.floor(seconds / 86400)
|
|
||||||
const hours = Math.floor((seconds % 86400) / 3600)
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60)
|
|
||||||
|
|
||||||
const parts = []
|
|
||||||
if (days > 0) {
|
|
||||||
parts.push(`${days} day${days === 1 ? "" : "s"}`)
|
|
||||||
}
|
|
||||||
if (hours > 0) {
|
|
||||||
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`)
|
|
||||||
}
|
|
||||||
if (minutes > 0) {
|
|
||||||
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length > 0) {
|
|
||||||
root.uptime = `up ${parts.join(", ")}`
|
|
||||||
} else {
|
|
||||||
root.uptime = `up ${seconds} seconds`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create short uptime format
|
|
||||||
let shortUptime = "up"
|
|
||||||
if (days > 0) {
|
|
||||||
shortUptime += ` ${days}d`
|
|
||||||
}
|
|
||||||
if (hours > 0) {
|
|
||||||
shortUptime += ` ${hours}h`
|
|
||||||
}
|
|
||||||
if (minutes > 0) {
|
|
||||||
shortUptime += ` ${minutes}m`
|
|
||||||
}
|
|
||||||
root.shortUptime = shortUptime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
// Minimal VPN controller backed by NetworkManager (nmcli + D-Bus monitor)
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// State
|
|
||||||
property bool available: true
|
|
||||||
property bool isBusy: false
|
|
||||||
property string errorMessage: ""
|
|
||||||
|
|
||||||
// Profiles discovered on the system
|
|
||||||
// [{ name, uuid, type }]
|
|
||||||
property var profiles: []
|
|
||||||
|
|
||||||
// Allow multiple active VPNs (set true to allow concurrent connections)
|
|
||||||
// Default: allow multiple, to align with NetworkManager capability
|
|
||||||
property bool singleActive: false
|
|
||||||
|
|
||||||
// Active VPN connections (may be multiple)
|
|
||||||
// Full list and convenience projections
|
|
||||||
property var activeConnections: [] // [{ name, uuid, device, state }]
|
|
||||||
property var activeUuids: []
|
|
||||||
property var activeNames: []
|
|
||||||
// Back-compat single values (first active if present)
|
|
||||||
property string activeUuid: activeUuids.length > 0 ? activeUuids[0] : ""
|
|
||||||
property string activeName: activeNames.length > 0 ? activeNames[0] : ""
|
|
||||||
property string activeDevice: activeConnections.length > 0 ? (activeConnections[0].device || "") : ""
|
|
||||||
property string activeState: activeConnections.length > 0 ? (activeConnections[0].state || "") : ""
|
|
||||||
property bool connected: activeUuids.length > 0
|
|
||||||
|
|
||||||
// Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.)
|
|
||||||
|
|
||||||
Component.onCompleted: initialize()
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
nmMonitor.running = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function initialize() {
|
|
||||||
// Start monitoring NetworkManager for changes
|
|
||||||
nmMonitor.running = true
|
|
||||||
refreshAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshAll() {
|
|
||||||
listProfiles()
|
|
||||||
refreshActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitor NetworkManager changes and refresh on activity
|
|
||||||
Process {
|
|
||||||
id: nmMonitor
|
|
||||||
command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.NetworkManager"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
onRead: line => {
|
|
||||||
if (line.includes("ActiveConnection") || line.includes("PropertiesChanged") || line.includes("StateChanged")) {
|
|
||||||
refreshAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query all VPN profiles
|
|
||||||
function listProfiles() {
|
|
||||||
getProfiles.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: getProfiles
|
|
||||||
command: ["bash", "-lc", "nmcli -t -f NAME,UUID,TYPE connection show | while IFS=: read -r name uuid type; do case \"$type\" in vpn) svc=$(nmcli -g vpn.service-type connection show uuid \"$uuid\" 2>/dev/null); echo \"$name:$uuid:$type:$svc\" ;; wireguard) echo \"$name:$uuid:$type:\" ;; *) : ;; esac; done"]
|
|
||||||
running: false
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const lines = text.trim().length ? text.trim().split('\n') : []
|
|
||||||
const out = []
|
|
||||||
for (const line of lines) {
|
|
||||||
const parts = line.split(':')
|
|
||||||
if (parts.length >= 3 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
|
|
||||||
const svc = parts.length >= 4 ? parts[3] : ""
|
|
||||||
out.push({ name: parts[0], uuid: parts[1], type: parts[2], serviceType: svc })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root.profiles = out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query active VPN connection
|
|
||||||
function refreshActive() {
|
|
||||||
getActive.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: getActive
|
|
||||||
command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE,DEVICE,STATE", "connection", "show", "--active"]
|
|
||||||
running: false
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const lines = text.trim().length ? text.trim().split('\n') : []
|
|
||||||
let act = []
|
|
||||||
for (const line of lines) {
|
|
||||||
const parts = line.split(':')
|
|
||||||
if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
|
|
||||||
act.push({ name: parts[0], uuid: parts[1], device: parts[3], state: parts[4] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root.activeConnections = act
|
|
||||||
root.activeUuids = act.map(a => a.uuid).filter(u => !!u)
|
|
||||||
root.activeNames = act.map(a => a.name).filter(n => !!n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActiveUuid(uuid) {
|
|
||||||
return root.activeUuids && root.activeUuids.indexOf(uuid) !== -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function _looksLikeUuid(s) {
|
|
||||||
// Very loose check for UUID pattern
|
|
||||||
return s && s.indexOf('-') !== -1 && s.length >= 8
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect(uuidOrName) {
|
|
||||||
if (root.isBusy) return
|
|
||||||
root.isBusy = true
|
|
||||||
root.errorMessage = ""
|
|
||||||
if (root.singleActive) {
|
|
||||||
// Bring down all active VPNs, then bring up the requested one
|
|
||||||
const isUuid = _looksLikeUuid(uuidOrName)
|
|
||||||
const escaped = ('' + uuidOrName).replace(/'/g, "'\\''")
|
|
||||||
const upCmd = isUuid ? `nmcli connection up uuid '${escaped}'` : `nmcli connection up id '${escaped}'`
|
|
||||||
const script = `set -e\n` +
|
|
||||||
`nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done\n` +
|
|
||||||
upCmd + `\n`
|
|
||||||
vpnSwitch.command = ["bash", "-lc", script]
|
|
||||||
vpnSwitch.running = true
|
|
||||||
} else {
|
|
||||||
if (_looksLikeUuid(uuidOrName)) {
|
|
||||||
vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName]
|
|
||||||
} else {
|
|
||||||
vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName]
|
|
||||||
}
|
|
||||||
vpnUp.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnect(uuidOrName) {
|
|
||||||
if (root.isBusy) return
|
|
||||||
root.isBusy = true
|
|
||||||
root.errorMessage = ""
|
|
||||||
if (_looksLikeUuid(uuidOrName)) {
|
|
||||||
vpnDown.command = ["nmcli", "connection", "down", "uuid", uuidOrName]
|
|
||||||
} else {
|
|
||||||
vpnDown.command = ["nmcli", "connection", "down", "id", uuidOrName]
|
|
||||||
}
|
|
||||||
vpnDown.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(uuid) {
|
|
||||||
if (uuid) {
|
|
||||||
if (isActiveUuid(uuid)) disconnect(uuid)
|
|
||||||
else connect(uuid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (root.profiles.length > 0) {
|
|
||||||
connect(root.profiles[0].uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: vpnUp
|
|
||||||
running: false
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.isBusy = false
|
|
||||||
if (!text.toLowerCase().includes("successfully")) {
|
|
||||||
root.errorMessage = text.trim()
|
|
||||||
}
|
|
||||||
refreshAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: exitCode => {
|
|
||||||
root.isBusy = false
|
|
||||||
if (exitCode !== 0 && root.errorMessage === "") {
|
|
||||||
root.errorMessage = "Failed to connect VPN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: vpnDown
|
|
||||||
running: false
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.isBusy = false
|
|
||||||
if (!text.toLowerCase().includes("deactivated") && !text.toLowerCase().includes("successfully")) {
|
|
||||||
root.errorMessage = text.trim()
|
|
||||||
}
|
|
||||||
refreshAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: exitCode => {
|
|
||||||
root.isBusy = false
|
|
||||||
if (exitCode !== 0 && root.errorMessage === "") {
|
|
||||||
root.errorMessage = "Failed to disconnect VPN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectAllActive() {
|
|
||||||
if (root.isBusy) return
|
|
||||||
root.isBusy = true
|
|
||||||
const script = `nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done`
|
|
||||||
vpnSwitch.command = ["bash", "-lc", script]
|
|
||||||
vpnSwitch.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sequenced down/up using a single shell for exclusive switch
|
|
||||||
Process {
|
|
||||||
id: vpnSwitch
|
|
||||||
running: false
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.isBusy = false
|
|
||||||
refreshAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: exitCode => {
|
|
||||||
root.isBusy = false
|
|
||||||
if (exitCode !== 0 && root.errorMessage === "") {
|
|
||||||
root.errorMessage = "Failed to switch VPN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,117 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string pickerTitle: "Choose Color"
|
|
||||||
property color selectedColor: Theme.primary
|
|
||||||
property bool isOpen: false
|
|
||||||
|
|
||||||
signal colorSelected(color selectedColor)
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
customColorField.text = ""
|
|
||||||
isOpen = true
|
|
||||||
Qt.callLater(() => root.forceActiveFocus())
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
isOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 320
|
|
||||||
height: 340
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
visible: isOpen
|
|
||||||
focus: isOpen
|
|
||||||
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
close()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
buttonSize: 28
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: 16
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: root.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: pickerTitle
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid {
|
|
||||||
columns: 8
|
|
||||||
spacing: 4
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
property var colors: ["#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722", "#795548", "#9e9e9e", "#607d8b", "#000000", "#ffffff", "#ff1744", "#f50057", "#d500f9", "#651fff", "#3d5afe", "#2979ff", "#00b0ff", "#00e5ff", "#1de9b6", "#00e676", "#76ff03", "#c6ff00", "#ffff00", "#ffc400", "#ff9100", "#ff3d00", "#bf360c", "#424242", "#37474f"]
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: parent.colors
|
|
||||||
Rectangle {
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
color: modelData
|
|
||||||
radius: 4
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: root.selectedColor == modelData ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.selectedColor = modelData
|
|
||||||
root.colorSelected(modelData)
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Custom Color:"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: customColorField
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
placeholderText: "#ff0000"
|
|
||||||
text: ""
|
|
||||||
onAccepted: {
|
|
||||||
var hexColor = text.startsWith("#") ? text : "#" + text
|
|
||||||
if (/^#[0-9A-Fa-f]{6}$/.test(hexColor)) {
|
|
||||||
root.selectedColor = hexColor
|
|
||||||
root.colorSelected(hexColor)
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
import "../Common/fzf.js" as Fzf
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string text: ""
|
|
||||||
property string description: ""
|
|
||||||
property string currentValue: ""
|
|
||||||
property var options: []
|
|
||||||
property var optionIcons: []
|
|
||||||
property bool forceRecreate: false
|
|
||||||
property bool enableFuzzySearch: false
|
|
||||||
property int popupWidthOffset: 0
|
|
||||||
property int maxPopupHeight: 400
|
|
||||||
|
|
||||||
signal valueChanged(string value)
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
Component.onCompleted: forceRecreateTimer.start()
|
|
||||||
Component.onDestruction: {
|
|
||||||
const popup = popupLoader.item
|
|
||||||
if (popup && popup.visible) {
|
|
||||||
popup.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
const popup = popupLoader.item
|
|
||||||
if (!visible && popup && popup.visible) {
|
|
||||||
popup.close()
|
|
||||||
} else if (visible) {
|
|
||||||
forceRecreateTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: forceRecreateTimer
|
|
||||||
|
|
||||||
interval: 50
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.forceRecreate = !root.forceRecreate
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: dropdown.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.text
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.description
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: description.length > 0
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: dropdown
|
|
||||||
|
|
||||||
width: root.width <= 60 ? root.width : 180
|
|
||||||
height: 36
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: dropdownArea.containsMouse ? Theme.primaryHover : Theme.contentBackground()
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dropdownArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
const popup = popupLoader.item
|
|
||||||
if (!popup) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popup.visible) {
|
|
||||||
popup.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4)
|
|
||||||
popup.x = pos.x - (root.popupWidthOffset / 2)
|
|
||||||
popup.y = pos.y
|
|
||||||
popup.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
const currentIndex = root.options.indexOf(root.currentValue)
|
|
||||||
return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : ""
|
|
||||||
}
|
|
||||||
size: 18
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: name !== "" && root.width > 60
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.currentValue
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: root.width <= 60 ? dropdown.width - expandIcon.width - Theme.spacingS * 2 : dropdown.width - contentRow.x - expandIcon.width - Theme.spacingM - Theme.spacingS
|
|
||||||
elide: root.width <= 60 ? Text.ElideNone : Text.ElideRight
|
|
||||||
horizontalAlignment: root.width <= 60 ? Text.AlignHCenter : Text.AlignLeft
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: expandIcon
|
|
||||||
|
|
||||||
name: "expand_more"
|
|
||||||
size: 20
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: popupLoader
|
|
||||||
|
|
||||||
property bool recreateFlag: root.forceRecreate
|
|
||||||
|
|
||||||
active: true
|
|
||||||
onRecreateFlagChanged: {
|
|
||||||
active = false
|
|
||||||
active = true
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
Popup {
|
|
||||||
id: dropdownMenu
|
|
||||||
|
|
||||||
property string searchQuery: ""
|
|
||||||
property var filteredOptions: []
|
|
||||||
property int selectedIndex: -1
|
|
||||||
property var fzfFinder: new Fzf.Finder(root.options, {
|
|
||||||
"selector": option => option,
|
|
||||||
"limit": 50,
|
|
||||||
"casing": "case-insensitive"
|
|
||||||
})
|
|
||||||
|
|
||||||
function updateFilteredOptions() {
|
|
||||||
if (!root.enableFuzzySearch || searchQuery.length === 0) {
|
|
||||||
filteredOptions = root.options
|
|
||||||
selectedIndex = -1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = fzfFinder.find(searchQuery)
|
|
||||||
filteredOptions = results.map(result => result.item)
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (filteredOptions.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
selectedIndex = (selectedIndex + 1) % filteredOptions.length
|
|
||||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (filteredOptions.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1
|
|
||||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectCurrent() {
|
|
||||||
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
root.currentValue = filteredOptions[selectedIndex]
|
|
||||||
root.valueChanged(filteredOptions[selectedIndex])
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
parent: Overlay.overlay
|
|
||||||
width: dropdown.width + root.popupWidthOffset
|
|
||||||
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
|
|
||||||
padding: 0
|
|
||||||
modal: true
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
onOpened: {
|
|
||||||
searchQuery = ""
|
|
||||||
updateFilteredOptions()
|
|
||||||
if (root.enableFuzzySearch && searchField.visible) {
|
|
||||||
searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: Rectangle {
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: searchContainer
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 42
|
|
||||||
visible: root.enableFuzzySearch
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceVariantAlpha
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 1
|
|
||||||
placeholderText: "Search..."
|
|
||||||
text: searchQuery
|
|
||||||
topPadding: Theme.spacingS
|
|
||||||
bottomPadding: Theme.spacingS
|
|
||||||
onTextChanged: {
|
|
||||||
searchQuery = text
|
|
||||||
updateFilteredOptions()
|
|
||||||
}
|
|
||||||
Keys.onDownPressed: selectNext()
|
|
||||||
Keys.onUpPressed: selectPrevious()
|
|
||||||
Keys.onReturnPressed: selectCurrent()
|
|
||||||
Keys.onEnterPressed: selectCurrent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 1
|
|
||||||
height: Theme.spacingXS
|
|
||||||
visible: root.enableFuzzySearch
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: listView
|
|
||||||
|
|
||||||
property var popupRef: dropdownMenu
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0)
|
|
||||||
clip: true
|
|
||||||
model: filteredOptions
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
interactive: true
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
|
||||||
pressDelay: 0
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
property bool isSelected: selectedIndex === index
|
|
||||||
property bool isCurrentValue: root.currentValue === modelData
|
|
||||||
property int optionIndex: root.options.indexOf(modelData)
|
|
||||||
|
|
||||||
width: ListView.view.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : ""
|
|
||||||
size: 18
|
|
||||||
color: isCurrentValue ? Theme.primary : Theme.surfaceVariantText
|
|
||||||
visible: name !== ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: modelData
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: isCurrentValue ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: isCurrentValue ? Font.Medium : Font.Normal
|
|
||||||
width: parent.parent.width - parent.x - Theme.spacingS
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: optionArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.currentValue = modelData
|
|
||||||
root.valueChanged(modelData)
|
|
||||||
listView.popupRef.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: icon
|
|
||||||
|
|
||||||
property alias name: icon.text
|
|
||||||
property alias size: icon.font.pixelSize
|
|
||||||
property alias color: icon.color
|
|
||||||
property bool filled: false
|
|
||||||
property real fill: filled ? 1.0 : 0.0
|
|
||||||
property int grade: Theme.isLightMode ? 0 : -25
|
|
||||||
property int weight: filled ? 500 : 400
|
|
||||||
|
|
||||||
font.family: "Material Symbols Rounded"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: weight
|
|
||||||
color: Theme.surfaceText
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
renderType: Text.NativeRendering
|
|
||||||
antialiasing: true
|
|
||||||
font.variableAxes: {
|
|
||||||
"FILL": fill.toFixed(1),
|
|
||||||
"GRAD": grade,
|
|
||||||
"opsz": 24,
|
|
||||||
"wght": weight
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on fill {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on weight {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user