mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
Compare commits
766 Commits
v0.2.0
...
468e569bc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
468e569bc7 | ||
|
|
139c99001a | ||
|
|
bd99be15c2 | ||
|
|
1d91d8fd94 | ||
|
|
f425f86101 | ||
|
|
83a6b7567f | ||
|
|
9184c70883 | ||
|
|
f5ca4ccce5 | ||
|
|
50f174be92 | ||
|
|
e5d11ce535 | ||
|
|
94851a51aa | ||
|
|
cfc07f4411 | ||
|
|
c6e9abda9f | ||
|
|
25951ddc55 | ||
|
|
bcd9ece077 | ||
|
|
68adbc38ba | ||
|
|
79a4d06cc0 | ||
|
|
18bf3b7548 | ||
|
|
4e66d3532e | ||
|
|
1b6d567451 | ||
|
|
7959a79575 | ||
|
|
abf3249b67 | ||
|
|
35e0dc84e8 | ||
|
|
17639e8729 | ||
|
|
cbd1fd908c | ||
|
|
b2cf20f3d8 | ||
|
|
915f1a5036 | ||
|
|
a55ec6416c | ||
|
|
b1834b1958 | ||
|
|
1beeb9fb55 | ||
|
|
18d86354ec | ||
|
|
6297b0679c | ||
|
|
d62ef635a7 | ||
|
|
c53836040f | ||
|
|
0b638bf85f | ||
|
|
7f6a71b964 | ||
|
|
1b4363a54a | ||
|
|
16d168c970 | ||
|
|
4606d7960e | ||
|
|
4eee126d26 | ||
|
|
dde426658f | ||
|
|
f6874fbcad | ||
|
|
621d4e4d92 | ||
|
|
76062231fd | ||
|
|
261f55fea5 | ||
|
|
202cf4bcc9 | ||
|
|
b7572f727f | ||
|
|
50ab346d58 | ||
|
|
b11b375848 | ||
|
|
e6c3ae9397 | ||
|
|
df663aceb9 | ||
|
|
db7e597f67 | ||
|
|
1d3fe81ff7 | ||
|
|
9c887fbe63 | ||
|
|
4723bffcd2 | ||
|
|
9643de3ca0 | ||
|
|
3bf3a54916 | ||
|
|
bcffc8856a | ||
|
|
6b8c35c27b | ||
|
|
dd409b4d1c | ||
|
|
94a1aebe2b | ||
|
|
d3030c3ec6 | ||
|
|
0221021078 | ||
|
|
966021bfd4 | ||
|
|
f06e6e85d5 | ||
|
|
28ad641070 | ||
|
|
384c775f1a | ||
|
|
ce40c691e9 | ||
|
|
5b0c38b0ed | ||
|
|
734456785f | ||
|
|
4f24312432 | ||
|
|
d79b1ff3b4 | ||
|
|
bbe1c1f1e0 | ||
|
|
1978e67401 | ||
|
|
e129e4a2d0 | ||
|
|
f7f1bbbdd2 | ||
|
|
de8f2e6a68 | ||
|
|
85704e3947 | ||
|
|
4d661ff41d | ||
|
|
d7b39634e6 | ||
|
|
039c98b9e3 | ||
|
|
172c4bf0a9 | ||
|
|
1f2a1c5dec | ||
|
|
e5a6a00282 | ||
|
|
d8153f7611 | ||
|
|
8b6ae3f39b | ||
|
|
24537781b7 | ||
|
|
d2a29506aa | ||
|
|
adf51d5264 | ||
|
|
0864179085 | ||
|
|
8de77f283d | ||
|
|
004a014000 | ||
|
|
80f6eb94aa | ||
|
|
4035c9cc5f | ||
|
|
3a365f6807 | ||
|
|
9920a0a59f | ||
|
|
c17bb9e171 | ||
|
|
03073f6875 | ||
|
|
609caf6e5f | ||
|
|
411141ff88 | ||
|
|
3e472e18bd | ||
|
|
e5b6fbd12a | ||
|
|
c2787f1282 | ||
|
|
df940124b1 | ||
|
|
5288d042ca | ||
|
|
fa98a27c90 | ||
|
|
d341a5a60b | ||
|
|
7f15227de1 | ||
|
|
bb45240665 | ||
|
|
29f84aeab5 | ||
|
|
5a52edcad8 | ||
|
|
b078e23aa1 | ||
|
|
7fa87125b5 | ||
|
|
f618df46d8 | ||
|
|
ee03853901 | ||
|
|
6c4a9bcfb8 | ||
|
|
1bec20ecef | ||
|
|
08c9bf570d | ||
|
|
5e77a10a81 | ||
|
|
3bc6461e2a | ||
|
|
d3194e15e2 | ||
|
|
2db79ef202 | ||
|
|
b3c07edef6 | ||
|
|
b773fdca34 | ||
|
|
2e9f9f7b7e | ||
|
|
30cbfe729d | ||
|
|
b036da2446 | ||
|
|
c8a9fb1674 | ||
|
|
43bea80cad | ||
|
|
23538c0323 | ||
|
|
2ae911230d | ||
|
|
5ce1cb87ea | ||
|
|
2a37028b6a | ||
|
|
8130feb2a0 | ||
|
|
c49a875ec2 | ||
|
|
2a002304b9 | ||
|
|
d9522818ae | ||
|
|
800588e121 | ||
|
|
991c31ebdb | ||
|
|
48f77e1691 | ||
|
|
42de6fd074 | ||
|
|
62845b470c | ||
|
|
fd20986cf8 | ||
|
|
61369cde9e | ||
|
|
644384ce8b | ||
|
|
97c11a2482 | ||
|
|
1e7e1c2d78 | ||
|
|
1c7201fb04 | ||
|
|
61ec0c697a | ||
|
|
4b5fce1bfc | ||
|
|
6cc6e7c8e9 | ||
|
|
89298fce30 | ||
|
|
a3a27e07fa | ||
|
|
4f32376f22 | ||
|
|
58bf189941 | ||
|
|
bcfa508da5 | ||
|
|
c0ae3ef58b | ||
|
|
1e70d7b4c3 | ||
|
|
f8dc6ad2bc | ||
|
|
e22482988f | ||
|
|
4eb896629d | ||
|
|
b310e66275 | ||
|
|
b39da1bea7 | ||
|
|
fa575d0574 | ||
|
|
dfe2f3771b | ||
|
|
46caeb0445 | ||
|
|
59cc9c7006 | ||
|
|
12e91534eb | ||
|
|
d9da88ceb5 | ||
|
|
2dbfec0307 | ||
|
|
09cf8c9641 | ||
|
|
f1bed4d6a3 | ||
|
|
2ed6c33c83 | ||
|
|
7ad532ed17 | ||
|
|
92fe8c5b14 | ||
|
|
8e95572589 | ||
|
|
62da862a66 | ||
|
|
993e34f548 | ||
|
|
e39465aece | ||
|
|
8fd616b680 | ||
|
|
cc054b27de | ||
|
|
dfdaa82245 | ||
|
|
99a307e0ad | ||
|
|
5ddea836a1 | ||
|
|
208d92aa06 | ||
|
|
6ef9ddd4f3 | ||
|
|
1c92d39185 | ||
|
|
c0f072217c | ||
|
|
542562f988 | ||
|
|
4e6f0d5e87 | ||
|
|
10639a5ead | ||
|
|
06d668e710 | ||
|
|
d1472dfcba | ||
|
|
ccb4da3cd8 | ||
|
|
46e96b49f0 | ||
|
|
984cfe7f98 | ||
|
|
d769300137 | ||
|
|
d175d66828 | ||
|
|
c1a314332e | ||
|
|
046ac59d21 | ||
|
|
00c06f07d0 | ||
|
|
3e2ab40c6a | ||
|
|
350ffd0052 | ||
|
|
ecd1a622d2 | ||
|
|
f13968aa61 | ||
|
|
4d1ffde54c | ||
|
|
d69017a706 | ||
|
|
f2deaeccdb | ||
|
|
ea9b0d2a79 | ||
|
|
2e6dbedb8b | ||
|
|
6f359df8f9 | ||
|
|
f6db20cd06 | ||
|
|
6287fae065 | ||
|
|
e441607ce3 | ||
|
|
b5379a95fa | ||
|
|
64ec5be919 | ||
|
|
3916512d66 | ||
|
|
e2f426a1bd | ||
|
|
aa1df8dfcf | ||
|
|
67557555f2 | ||
|
|
4cb652abd9 | ||
|
|
d11868b99f | ||
|
|
1798417e6a | ||
|
|
43dc3e5bb1 | ||
|
|
91891a14ed | ||
|
|
20f7d60147 | ||
|
|
7e17e7d37a | ||
|
|
cbb244f785 | ||
|
|
1c264d858b | ||
|
|
217037c2ae | ||
|
|
b4dbd0b69c | ||
|
|
89a2b5c00b | ||
|
|
929b6dae1a | ||
|
|
52fe493da9 | ||
|
|
3e6be3e762 | ||
|
|
7a8cc449b9 | ||
|
|
8f5a9d6e9f | ||
|
|
1c5e31fea9 | ||
|
|
fd08ae18ab | ||
|
|
a7eb3de06e | ||
|
|
8902dd7c44 | ||
|
|
6387d8400c | ||
|
|
597cacb9cc | ||
|
|
3e285ad9ff | ||
|
|
cc1fa89790 | ||
|
|
b0ed007751 | ||
|
|
e1e2650d2b | ||
|
|
b23f17b633 | ||
|
|
818e40b2df | ||
|
|
5685e39631 | ||
|
|
72534b7674 | ||
|
|
328490d23d | ||
|
|
97a0696930 | ||
|
|
cb4e0660e0 | ||
|
|
67c642de4c | ||
|
|
0d7c2e1024 | ||
|
|
16a779a41b | ||
|
|
c4ca3c8644 | ||
|
|
aabcbe34f3 | ||
|
|
f06626e441 | ||
|
|
c4e1a71776 | ||
|
|
77e6c16bd2 | ||
|
|
9d1fac3570 | ||
|
|
b7aeaa7fc5 | ||
|
|
f6d8c9ff61 | ||
|
|
0490794d6c | ||
|
|
335c83dd3c | ||
|
|
91da720c26 | ||
|
|
b6ac744a68 | ||
|
|
526c4092fd | ||
|
|
ed06dda384 | ||
|
|
6465b11e9b | ||
|
|
b2879878a1 | ||
|
|
3e17b086fb | ||
|
|
0545e6bcda | ||
|
|
27a907433f | ||
|
|
69616800e3 | ||
|
|
abf1f53432 | ||
|
|
881c5f75cb | ||
|
|
4e45796ade | ||
|
|
1ce4ea5230 | ||
|
|
f2a2437baa | ||
|
|
508dc9db1e | ||
|
|
a914e3557f | ||
|
|
f489dc062f | ||
|
|
a7e09f4850 | ||
|
|
8ea97530d4 | ||
|
|
13ab54e83a | ||
|
|
4bc40325cb | ||
|
|
58d9355ea3 | ||
|
|
d46b7528e7 | ||
|
|
1858597fc9 | ||
|
|
83cce5afe4 | ||
|
|
201bd8dc1f | ||
|
|
b62ba69060 | ||
|
|
5d2f5557e5 | ||
|
|
cf75c1aad0 | ||
|
|
76a60df88b | ||
|
|
9322c79b4e | ||
|
|
12365edcf0 | ||
|
|
5efc1f9dad | ||
|
|
ab976cbb24 | ||
|
|
db584b7897 | ||
|
|
0fdc0748cf | ||
|
|
2e79c21dc2 | ||
|
|
5490a230bd | ||
|
|
a6b059b30d | ||
|
|
712e6011aa | ||
|
|
68f6f87410 | ||
|
|
50cdd68b7b | ||
|
|
e8510b925e | ||
|
|
24e800501a | ||
|
|
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 |
66
.githooks/pre-commit
Executable file
66
.githooks/pre-commit
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Go CI checks (when core/ files are staged)
|
||||||
|
# =============================================================================
|
||||||
|
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
||||||
|
|
||||||
|
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
||||||
|
echo "Go files staged in core/, running CI checks..."
|
||||||
|
cd "$REPO_ROOT/core"
|
||||||
|
|
||||||
|
# Format check
|
||||||
|
echo " Checking gofmt..."
|
||||||
|
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
||||||
|
if [[ -n "$UNFORMATTED" ]]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
echo "$UNFORMATTED"
|
||||||
|
echo ""
|
||||||
|
echo "Run: cd core && gofmt -s -w ."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# golangci-lint
|
||||||
|
if command -v golangci-lint &>/dev/null; then
|
||||||
|
echo " Running golangci-lint..."
|
||||||
|
golangci-lint run ./...
|
||||||
|
else
|
||||||
|
echo " Warning: golangci-lint not installed, skipping lint"
|
||||||
|
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
echo " Running tests..."
|
||||||
|
go test ./... > /dev/null
|
||||||
|
|
||||||
|
# Build checks
|
||||||
|
echo " Building..."
|
||||||
|
mkdir -p bin
|
||||||
|
go build -buildvcs=false -o bin/dms ./cmd/dms
|
||||||
|
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
|
||||||
|
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
|
||||||
|
|
||||||
|
echo "All Go CI checks passed!"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# i18n sync check (DISABLED for now)
|
||||||
|
# =============================================================================
|
||||||
|
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
|
||||||
|
# if command -v python3 &>/dev/null; then
|
||||||
|
# if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
||||||
|
# echo "Translations out of sync"
|
||||||
|
# echo "Run: python3 scripts/i18nsync.py sync"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
# 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']
|
||||||
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -9,21 +9,15 @@ 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
|
## Distribution
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/support_request.md
vendored
2
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -10,6 +10,8 @@ assignees: ""
|
|||||||
|
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
- [ ] other
|
- [ ] other
|
||||||
|
|
||||||
## Distribution
|
## Distribution
|
||||||
|
|||||||
60
.github/workflows/go-ci.yml
vendored
Normal file
60
.github/workflows/go-ci.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Go CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
paths:
|
||||||
|
- "core/**"
|
||||||
|
- ".github/workflows/go-ci.yml"
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
paths:
|
||||||
|
- "core/**"
|
||||||
|
- ".github/workflows/go-ci.yml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: go-ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: 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: Run golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: v2.6
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: Build dms
|
||||||
|
run: go build -v ./cmd/dms
|
||||||
|
|
||||||
|
- name: Build dms (distropkg)
|
||||||
|
run: go build -v -tags distro_binary ./cmd/dms
|
||||||
|
|
||||||
|
- name: Build dankinstall
|
||||||
|
run: go build -v ./cmd/dankinstall
|
||||||
189
.github/workflows/poeditor-export.yml
vendored
189
.github/workflows/poeditor-export.yml
vendored
@@ -1,189 +0,0 @@
|
|||||||
name: POEditor Diff & Sync
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
workflow_dispatch: {}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: poeditor-sync
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-translations:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.x'
|
|
||||||
|
|
||||||
- name: Install jq
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y jq
|
|
||||||
|
|
||||||
- name: Extract source strings from codebase
|
|
||||||
env:
|
|
||||||
API_TOKEN: ${{ secrets.POEDITOR_API_TOKEN }}
|
|
||||||
PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
echo "::group::Extracting strings from QML files"
|
|
||||||
python3 translations/extract_translations.py
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
echo "::group::Checking for changes in en.json"
|
|
||||||
if [[ -f "translations/en.json" ]]; then
|
|
||||||
jq -S . "translations/en.json" > /tmp/en_new.json
|
|
||||||
if [[ -f "translations/en.json.orig" ]]; then
|
|
||||||
jq -S . "translations/en.json.orig" > /tmp/en_old.json
|
|
||||||
else
|
|
||||||
git show HEAD:translations/en.json > /tmp/en_old.json 2>/dev/null || echo "[]" > /tmp/en_old.json
|
|
||||||
jq -S . /tmp/en_old.json > /tmp/en_old.json.tmp && mv /tmp/en_old.json.tmp /tmp/en_old.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
if diff -q /tmp/en_new.json /tmp/en_old.json >/dev/null 2>&1; then
|
|
||||||
echo "No changes in source strings"
|
|
||||||
echo "source_changed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "Detected changes in source strings"
|
|
||||||
echo "source_changed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
echo "::group::Uploading source strings to POEditor"
|
|
||||||
RESP=$(curl -sS -X POST https://api.poeditor.com/v2/projects/upload \
|
|
||||||
-F api_token="$API_TOKEN" \
|
|
||||||
-F id="$PROJECT_ID" \
|
|
||||||
-F updating="terms" \
|
|
||||||
-F file=@"translations/en.json")
|
|
||||||
|
|
||||||
STATUS=$(echo "$RESP" | jq -r '.response.status')
|
|
||||||
if [[ "$STATUS" != "success" ]]; then
|
|
||||||
echo "::warning::POEditor upload failed: $RESP"
|
|
||||||
else
|
|
||||||
TERMS_ADDED=$(echo "$RESP" | jq -r '.result.terms.added // 0')
|
|
||||||
TERMS_UPDATED=$(echo "$RESP" | jq -r '.result.terms.updated // 0')
|
|
||||||
TERMS_DELETED=$(echo "$RESP" | jq -r '.result.terms.deleted // 0')
|
|
||||||
echo "Terms added: $TERMS_ADDED, updated: $TERMS_UPDATED, deleted: $TERMS_DELETED"
|
|
||||||
fi
|
|
||||||
echo "::endgroup::"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "::warning::translations/en.json not found"
|
|
||||||
echo "source_changed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
echo "::endgroup::"
|
|
||||||
id: extract
|
|
||||||
|
|
||||||
- name: Commit and push source strings
|
|
||||||
if: steps.extract.outputs.source_changed == 'true'
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
git add translations/en.json translations/template.json
|
|
||||||
git commit -m "i18n: update source strings from codebase"
|
|
||||||
|
|
||||||
for attempt in 1 2 3; do
|
|
||||||
if git push; then
|
|
||||||
echo "Successfully pushed source string updates"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Push attempt $attempt failed, pulling and retrying..."
|
|
||||||
git pull --rebase
|
|
||||||
sleep $((attempt*2))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Failed to push after retries" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Export and update translations from POEditor
|
|
||||||
env:
|
|
||||||
API_TOKEN: ${{ secrets.POEDITOR_API_TOKEN }}
|
|
||||||
PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
LANGUAGES=(
|
|
||||||
"ja:translations/poexports/ja.json"
|
|
||||||
"zh-Hans:translations/poexports/zh_CN.json"
|
|
||||||
"pt-br:translations/poexports/pt.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
ANY_CHANGED=false
|
|
||||||
|
|
||||||
for lang_pair in "${LANGUAGES[@]}"; do
|
|
||||||
IFS=':' read -r PO_LANG REPO_FILE <<< "$lang_pair"
|
|
||||||
|
|
||||||
echo "::group::Processing $PO_LANG"
|
|
||||||
|
|
||||||
RESP=$(curl -sS -X POST https://api.poeditor.com/v2/projects/export \
|
|
||||||
-d api_token="$API_TOKEN" \
|
|
||||||
-d id="$PROJECT_ID" \
|
|
||||||
-d language="$PO_LANG" \
|
|
||||||
-d type="key_value_json")
|
|
||||||
|
|
||||||
STATUS=$(echo "$RESP" | jq -r '.response.status')
|
|
||||||
if [[ "$STATUS" != "success" ]]; then
|
|
||||||
echo "POEditor export request failed for $PO_LANG: $RESP" >&2
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
URL=$(echo "$RESP" | jq -r '.result.url')
|
|
||||||
if [[ -z "$URL" || "$URL" == "null" ]]; then
|
|
||||||
echo "No export URL returned for $PO_LANG" >&2
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -sS -L "$URL" -o "/tmp/po_export_${PO_LANG}.json"
|
|
||||||
jq -S . "/tmp/po_export_${PO_LANG}.json" > "/tmp/po_export_${PO_LANG}.norm.json"
|
|
||||||
|
|
||||||
if [[ -f "$REPO_FILE" ]]; then
|
|
||||||
jq -S . "$REPO_FILE" > "/tmp/repo_${PO_LANG}.norm.json" || echo "{}" > "/tmp/repo_${PO_LANG}.norm.json"
|
|
||||||
else
|
|
||||||
echo "{}" > "/tmp/repo_${PO_LANG}.norm.json"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if diff -q "/tmp/po_export_${PO_LANG}.norm.json" "/tmp/repo_${PO_LANG}.norm.json" >/dev/null; then
|
|
||||||
echo "No changes for $PO_LANG"
|
|
||||||
else
|
|
||||||
echo "Detected changes for $PO_LANG"
|
|
||||||
mkdir -p "$(dirname "$REPO_FILE")"
|
|
||||||
cp "/tmp/po_export_${PO_LANG}.norm.json" "$REPO_FILE"
|
|
||||||
ANY_CHANGED=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "::endgroup::"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "any_changed=$ANY_CHANGED" >> "$GITHUB_OUTPUT"
|
|
||||||
id: export
|
|
||||||
|
|
||||||
- name: Commit and push translation updates
|
|
||||||
if: steps.export.outputs.any_changed == 'true'
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
git add translations/poexports/*.json
|
|
||||||
git commit -m "i18n: update translations"
|
|
||||||
|
|
||||||
for attempt in 1 2 3; do
|
|
||||||
if git push; then
|
|
||||||
echo "Successfully pushed translation updates"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Push attempt $attempt failed, pulling and retrying..."
|
|
||||||
git pull --rebase
|
|
||||||
sleep $((attempt*2))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Failed to push after retries" >&2
|
|
||||||
exit 1
|
|
||||||
626
.github/workflows/release.yml
vendored
626
.github/workflows/release.yml
vendored
@@ -1,46 +1,194 @@
|
|||||||
# Release from a dispatch event from the danklinux repo
|
name: Release
|
||||||
name: Create Release from DMS
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
repository_dispatch:
|
push:
|
||||||
types: [dms_release]
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.event.client_payload.tag }}
|
group: release-${{ github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_release_from_dms:
|
build-core:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
env:
|
strategy:
|
||||||
TAG: ${{ github.event.client_payload.tag }}
|
matrix:
|
||||||
DMS_REPO: ${{ github.event.client_payload.dms_repo }}
|
arch: [amd64, arm64]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Ensure VERSION and tag
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
git config user.name "github-actions[bot]"
|
echo "The following files are not formatted:"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
gofmt -s -l .
|
||||||
|
exit 1
|
||||||
echo "${TAG}" > VERSION
|
|
||||||
|
|
||||||
git add -A VERSION
|
|
||||||
|
|
||||||
if ! git diff --cached --quiet; then
|
|
||||||
git commit -m "Update VERSION to ${TAG} (from DMS)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git tag -f "${TAG}"
|
- name: Run tests
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
git push origin HEAD
|
- name: Build dankinstall (${{ matrix.arch }})
|
||||||
git push -f origin "${TAG}"
|
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: core-assets-${{ matrix.arch }}
|
||||||
|
path: |
|
||||||
|
core/dankinstall-${{ matrix.arch }}.gz
|
||||||
|
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
|
core/dms-${{ matrix.arch }}.gz
|
||||||
|
core/dms-${{ matrix.arch }}.gz.sha256
|
||||||
|
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||||
|
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload artifacts with completions
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: core-assets-${{ matrix.arch }}
|
||||||
|
path: |
|
||||||
|
core/dankinstall-${{ matrix.arch }}.gz
|
||||||
|
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
|
core/dms-${{ matrix.arch }}.gz
|
||||||
|
core/dms-${{ matrix.arch }}.gz.sha256
|
||||||
|
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||||
|
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||||
|
core/completion.bash
|
||||||
|
core/completion.fish
|
||||||
|
core/completion.zsh
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
update-versions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-core
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Update VERSION
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
version="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "Updating to version: $version"
|
||||||
|
echo "${version}" > quickshell/VERSION
|
||||||
|
git add quickshell/VERSION
|
||||||
|
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git commit -m "chore: bump version to $version"
|
||||||
|
git pull --rebase origin master
|
||||||
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||||
|
fi
|
||||||
|
|
||||||
|
git tag -f "${version}"
|
||||||
|
git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs: [build-core, update-versions]
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fetch updated tag after version bump
|
||||||
|
run: |
|
||||||
|
git fetch origin --force tag ${{ github.ref_name }}
|
||||||
|
git checkout ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Download core artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: core-assets-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: ./_core_assets
|
||||||
|
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
@@ -48,23 +196,31 @@ jobs:
|
|||||||
set -e
|
set -e
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 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
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" | head -50)
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
||||||
else
|
else
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" "${PREVIOUS_TAG}..${TAG}")
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > RELEASE_BODY.md << 'EOF'
|
cat > RELEASE_BODY.md << 'EOF'
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://install.danklinux.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
## Assets
|
## Assets
|
||||||
|
|
||||||
### Complete Packages
|
### Complete Packages
|
||||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + installation guide)
|
- **`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 + installation guide)
|
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
|
||||||
### Individual Components
|
### Individual Components
|
||||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
- **`dms-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
|
- **`dms-qml.tar.gz`** - QML source code only
|
||||||
|
|
||||||
### Checksums
|
### Checksums
|
||||||
@@ -88,30 +244,14 @@ jobs:
|
|||||||
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create/Update DankMaterialShell Release
|
- name: Prepare release assets
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.TAG }}
|
|
||||||
name: Release ${{ env.TAG }}
|
|
||||||
body: ${{ steps.changelog.outputs.changelog }}
|
|
||||||
draft: false
|
|
||||||
prerelease: ${{ contains(env.TAG, '-') }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Download and prepare release assets
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
mkdir -p _release_assets
|
mkdir -p _release_assets
|
||||||
|
|
||||||
# Download DMS CLI binaries from the danklinux repo
|
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
|
||||||
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
|
for file in _core_assets/dms-*.gz*; do
|
||||||
|
|
||||||
# Rename CLI binaries to dms-cli-* format and copy distropkg binaries
|
|
||||||
for file in _dms_assets/dms-*.gz*; do
|
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
basename=$(basename "$file")
|
basename=$(basename "$file")
|
||||||
if [[ "$basename" == dms-distropkg-* ]]; then
|
if [[ "$basename" == dms-distropkg-* ]]; then
|
||||||
@@ -123,13 +263,21 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Create QML source package (exclude .git, .github, build artifacts)
|
# Copy dankinstall binaries
|
||||||
tar --exclude='.git' \
|
cp _core_assets/dankinstall-*.gz* _release_assets/
|
||||||
|
|
||||||
|
# Copy completions
|
||||||
|
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create QML source package (exclude build artifacts and git files)
|
||||||
|
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||||
|
cp LICENSE CONTRIBUTING.md quickshell/
|
||||||
|
|
||||||
|
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||||
|
(cd quickshell && tar --exclude='.git' \
|
||||||
--exclude='.github' \
|
--exclude='.github' \
|
||||||
--exclude='_dms_assets' \
|
|
||||||
--exclude='_release_assets' \
|
|
||||||
--exclude='*.tar.gz' \
|
--exclude='*.tar.gz' \
|
||||||
-czf _release_assets/dms-qml.tar.gz .
|
-czf ../_release_assets/dms-qml.tar.gz .)
|
||||||
|
|
||||||
# Generate checksum for QML package
|
# Generate checksum for QML package
|
||||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||||
@@ -138,24 +286,36 @@ jobs:
|
|||||||
for arch in amd64 arm64; do
|
for arch in amd64 arm64; do
|
||||||
mkdir -p _temp_full/dms
|
mkdir -p _temp_full/dms
|
||||||
mkdir -p _temp_full/bin
|
mkdir -p _temp_full/bin
|
||||||
|
mkdir -p _temp_full/completions
|
||||||
|
|
||||||
# Extract QML source to temp directory
|
# Extract QML source
|
||||||
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
||||||
|
|
||||||
# Copy CLI binary if it exists
|
# Add CLI binaries
|
||||||
if [ -f "_dms_assets/dms-${arch}.gz" ]; then
|
if [ -f "_core_assets/dms-${arch}.gz" ]; then
|
||||||
gunzip -c "_dms_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
gunzip -c "_core_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||||
chmod +x _temp_full/bin/dms
|
chmod +x _temp_full/bin/dms
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy distropkg binary if it exists
|
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
|
||||||
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
|
gunzip -c "_core_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||||
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
|
||||||
chmod +x _temp_full/bin/dms-distropkg
|
chmod +x _temp_full/bin/dms-distropkg
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create INSTALL.md
|
# Add shell completions
|
||||||
cat > _temp_full/INSTALL.md << 'EOF'
|
for completion in _core_assets/completion.*; do
|
||||||
|
if [ -f "$completion" ]; then
|
||||||
|
cp "$completion" _temp_full/completions/
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Copy docs directory
|
||||||
|
if [ -d "docs" ]; then
|
||||||
|
cp -r docs _temp_full/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create installation guide
|
||||||
|
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
|
||||||
# DankMaterialShell Installation
|
# DankMaterialShell Installation
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -175,16 +335,23 @@ jobs:
|
|||||||
2. **Install the DMS CLI binaries:**
|
2. **Install the DMS CLI binaries:**
|
||||||
```bash
|
```bash
|
||||||
sudo install -m 755 bin/dms /usr/local/bin/dms
|
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||||
# or install to a local directory:
|
|
||||||
mkdir -p ~/.local/bin
|
|
||||||
install -m 755 bin/dms ~/.local/bin/dms
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start the shell:**
|
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
|
```bash
|
||||||
dms run
|
dms run
|
||||||
# or directly with quickshell (will lack some dbus integrations & plugin management):
|
|
||||||
quickshell -p ~/.config/quickshell/dms
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -195,10 +362,9 @@ jobs:
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- Run with verbose output: `quickshell -v -p ~/.config/quickshell/dms`
|
- Run with verbose output: `DMS_LOG_LEVEL=debug dms run`
|
||||||
- Check logs in `~/.local/state/DankMaterialShell/`
|
|
||||||
- Ensure all dependencies are installed
|
- Ensure all dependencies are installed
|
||||||
EOF
|
EOFINSTALL
|
||||||
|
|
||||||
# Create the full package
|
# Create the full package
|
||||||
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
||||||
@@ -210,10 +376,324 @@ jobs:
|
|||||||
rm -rf _temp_full
|
rm -rf _temp_full
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Attach all assets to release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG }}
|
tag_name: ${{ env.TAG }}
|
||||||
|
name: Release ${{ env.TAG }}
|
||||||
|
body: ${{ steps.changelog.outputs.changelog }}
|
||||||
files: _release_assets/**
|
files: _release_assets/**
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ contains(env.TAG, '-') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
trigger-obs-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Update OBS packages
|
||||||
|
run: |
|
||||||
|
VERSION="${{ github.ref_name }}"
|
||||||
|
cd distro
|
||||||
|
bash scripts/obs-upload.sh dms "Update to $VERSION"
|
||||||
|
|
||||||
|
trigger-ppa-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
debhelper \
|
||||||
|
devscripts \
|
||||||
|
dput \
|
||||||
|
lftp \
|
||||||
|
build-essential \
|
||||||
|
fakeroot \
|
||||||
|
dpkg-dev
|
||||||
|
|
||||||
|
- name: Configure GPG
|
||||||
|
env:
|
||||||
|
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "$GPG_KEY" | gpg --import
|
||||||
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload to PPA
|
||||||
|
run: |
|
||||||
|
VERSION="${{ github.ref_name }}"
|
||||||
|
cd distro/ubuntu/ppa
|
||||||
|
bash create-and-upload.sh ../dms dms questing
|
||||||
|
|
||||||
|
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 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
|
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
|
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
|
||||||
|
%{_datadir}/applications/dms-open.desktop
|
||||||
|
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
|
%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:
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
name: DMS Copr Stable Release
|
name: DMS Copr Stable Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
release:
|
||||||
|
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
|
||||||
|
required: false
|
||||||
|
default: '1'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-upload:
|
build-and-upload:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -24,25 +23,23 @@ jobs:
|
|||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
|
# Get version from manual input or latest release
|
||||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
echo "Using manual version: $VERSION"
|
echo "Using manual version: $VERSION"
|
||||||
elif [ "${{ github.event_name }}" = "release" ]; then
|
|
||||||
VERSION="${{ github.event.release.tag_name }}"
|
|
||||||
VERSION="${VERSION#v}"
|
|
||||||
echo "Using release version: $VERSION"
|
|
||||||
elif [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
|
||||||
VERSION="${{ github.ref_name }}"
|
|
||||||
VERSION="${VERSION#v}"
|
|
||||||
echo "Using tag version: $VERSION"
|
|
||||||
else
|
else
|
||||||
# Fallback to latest release
|
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||||
VERSION=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
|
||||||
echo "Using latest release version: $VERSION"
|
echo "Using latest release version: $VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
RELEASE="${{ github.event.inputs.release }}"
|
||||||
|
if [ -z "$RELEASE" ]; then
|
||||||
|
RELEASE="1"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "✅ Building DMS stable version: $VERSION"
|
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||||
|
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
|
||||||
|
|
||||||
- name: Setup build environment
|
- name: Setup build environment
|
||||||
run: |
|
run: |
|
||||||
@@ -71,6 +68,7 @@ jobs:
|
|||||||
- name: Generate stable spec file
|
- name: Generate stable spec file
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
RELEASE="${{ steps.version.outputs.release }}"
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
|
|
||||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||||
@@ -82,27 +80,26 @@ jobs:
|
|||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Version: %{version}
|
Version: %{version}
|
||||||
Release: 1%{?dist}
|
Release: RELEASE_PLACEHOLDER%{?dist}
|
||||||
Summary: %{pkg_summary}
|
Summary: %{pkg_summary}
|
||||||
|
|
||||||
License: GPL-3.0-only
|
License: MIT
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
Source0: dms-qml.tar.gz
|
||||||
|
|
||||||
BuildRequires: gzip
|
BuildRequires: gzip
|
||||||
BuildRequires: wget
|
BuildRequires: wget
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
Requires: (quickshell or quickshell-git)
|
||||||
|
Requires: accountsservice
|
||||||
Requires: dms-cli
|
Requires: dms-cli
|
||||||
Requires: dgop
|
Requires: dgop
|
||||||
Requires: fira-code-fonts
|
|
||||||
Requires: material-symbols-fonts
|
|
||||||
Requires: rsms-inter-fonts
|
|
||||||
|
|
||||||
Recommends: brightnessctl
|
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
Recommends: cliphist
|
||||||
|
Recommends: danksearch
|
||||||
Recommends: hyprpicker
|
Recommends: hyprpicker
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: wl-clipboard
|
Recommends: wl-clipboard
|
||||||
@@ -121,8 +118,8 @@ jobs:
|
|||||||
|
|
||||||
%package -n dms-cli
|
%package -n dms-cli
|
||||||
Summary: DankMaterialShell CLI tool
|
Summary: DankMaterialShell CLI tool
|
||||||
License: GPL-3.0-only
|
License: MIT
|
||||||
URL: https://github.com/AvengeMedia/danklinux
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
%description -n dms-cli
|
%description -n dms-cli
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
Command-line interface for DankMaterialShell configuration and management.
|
||||||
@@ -158,7 +155,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
# Download dms-cli for target architecture
|
# Download dms-cli for target architecture
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
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}"
|
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -179,36 +176,65 @@ jobs:
|
|||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_sysconfdir}/xdg/quickshell/dms
|
# Shell completions
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/
|
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 || :
|
||||||
|
|
||||||
rm -rf %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.git*
|
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
rm -f %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.gitignore
|
|
||||||
rm -rf %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.github
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
rm -f %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/*.spec
|
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
|
%files
|
||||||
%license LICENSE
|
%license LICENSE
|
||||||
%doc README.md CONTRIBUTING.md
|
%doc README.md CONTRIBUTING.md
|
||||||
%{_sysconfdir}/xdg/quickshell/dms/
|
%{_datadir}/quickshell/dms/
|
||||||
|
%{_userunitdir}/dms.service
|
||||||
|
|
||||||
%files -n dms-cli
|
%files -n dms-cli
|
||||||
%{_bindir}/dms
|
%{_bindir}/dms
|
||||||
|
%{_datadir}/bash-completion/completions/dms
|
||||||
|
%{_datadir}/zsh/site-functions/_dms
|
||||||
|
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
%files -n dgop
|
%files -n dgop
|
||||||
%{_bindir}/dgop
|
%{_bindir}/dgop
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
||||||
- Stable release VERSION_PLACEHOLDER
|
- Stable release VERSION_PLACEHOLDER
|
||||||
- Built from GitHub release
|
- Built from GitHub release
|
||||||
- Includes latest dms-cli and dgop binaries
|
- Includes latest dms-cli and dgop binaries
|
||||||
SPECEOF
|
SPECEOF
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/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 "✅ Spec file generated for v${VERSION}-${RELEASE}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Spec file preview ==="
|
echo "=== Spec file preview ==="
|
||||||
head -40 ~/rpmbuild/SPECS/dms.spec
|
head -40 ~/rpmbuild/SPECS/dms.spec
|
||||||
@@ -282,7 +308,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
243
.github/workflows/run-obs.yml
vendored
Normal file
243
.github/workflows/run-obs.yml
vendored
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
name: Update OBS Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to update (dms, dms-git, or all)'
|
||||||
|
required: false
|
||||||
|
default: 'all'
|
||||||
|
rebuild_release:
|
||||||
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-updates:
|
||||||
|
name: Check for updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
|
version: ${{ steps.check.outputs.version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Check for updates
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by tag: $VERSION (always update)"
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Checking if dms-git source has changed..."
|
||||||
|
|
||||||
|
# Get latest commit hash from master branch
|
||||||
|
LATEST_COMMIT=$(git rev-parse origin/master 2>/dev/null || git rev-parse master 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -z "$LATEST_COMMIT" ]]; then
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Could not determine git commit, proceeding with update"
|
||||||
|
else
|
||||||
|
# Check OBS for last uploaded commit
|
||||||
|
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||||
|
mkdir -p "$OBS_BASE"
|
||||||
|
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||||
|
|
||||||
|
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||||
|
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||||
|
osc up -q 2>/dev/null || true
|
||||||
|
|
||||||
|
# Check tarball age - if older than 3 hours, update needed
|
||||||
|
if [[ -f "dms-git-source.tar.gz" ]]; then
|
||||||
|
TARBALL_MTIME=$(stat -c%Y "dms-git-source.tar.gz" 2>/dev/null || echo "0")
|
||||||
|
CURRENT_TIME=$(date +%s)
|
||||||
|
AGE_SECONDS=$((CURRENT_TIME - TARBALL_MTIME))
|
||||||
|
AGE_HOURS=$((AGE_SECONDS / 3600))
|
||||||
|
|
||||||
|
# If tarball is older than 3 hours, check for new commits
|
||||||
|
if [[ $AGE_HOURS -ge 3 ]]; then
|
||||||
|
# Check if there are new commits in the last 3 hours
|
||||||
|
cd "${{ github.workspace }}"
|
||||||
|
NEW_COMMITS=$(git log --since="3 hours ago" --oneline origin/master 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
if [[ $NEW_COMMITS -gt 0 ]]; then
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commits detected in last 3 hours, update needed"
|
||||||
|
else
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No new commits in last 3 hours, skipping update"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Recent upload exists (< 3 hours), skipping update"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No existing tarball in OBS, update needed"
|
||||||
|
fi
|
||||||
|
cd "${{ github.workspace }}"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 First upload to OBS, update needed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=all" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
update-obs:
|
||||||
|
name: Upload to OBS
|
||||||
|
needs: check-updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine packages to update
|
||||||
|
id: packages
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by tag: $VERSION"
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by schedule: updating git package"
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update dms-git spec version
|
||||||
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
|
run: |
|
||||||
|
# Get commit info for dms-git versioning
|
||||||
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||||
|
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
|
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Update version in spec
|
||||||
|
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
|
# Add changelog entry
|
||||||
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
|
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||||
|
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
|
- name: Update dms stable version
|
||||||
|
if: steps.packages.outputs.version != ''
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
|
VERSION_NO_V="${VERSION#v}"
|
||||||
|
echo "Updating packaging to version $VERSION_NO_V"
|
||||||
|
|
||||||
|
# Update openSUSE dms spec (stable only)
|
||||||
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||||
|
|
||||||
|
# Update Debian _service files
|
||||||
|
for service in distro/debian/*/_service; do
|
||||||
|
if [[ -f "$service" ]]; then
|
||||||
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Upload to OBS
|
||||||
|
env:
|
||||||
|
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
MESSAGE="Automated update from GitHub Actions"
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
||||||
|
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||||
|
else
|
||||||
|
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
||||||
123
.github/workflows/run-ppa.yml
vendored
Normal file
123
.github/workflows/run-ppa.yml
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
name: Update PPA Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
|
||||||
|
required: false
|
||||||
|
default: 'dms-git'
|
||||||
|
rebuild_release:
|
||||||
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload-ppa:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
debhelper \
|
||||||
|
devscripts \
|
||||||
|
dput \
|
||||||
|
lftp \
|
||||||
|
build-essential \
|
||||||
|
fakeroot \
|
||||||
|
dpkg-dev
|
||||||
|
|
||||||
|
- name: Configure GPG
|
||||||
|
env:
|
||||||
|
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "$GPG_KEY" | gpg --import
|
||||||
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Determine packages to upload
|
||||||
|
id: packages
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by schedule: uploading git package"
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload to PPA
|
||||||
|
env:
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms to PPA..."
|
||||||
|
if [ -n "$REBUILD_RELEASE" ]; then
|
||||||
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-git to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-greeter to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
|
||||||
|
else
|
||||||
|
PPA_NAME="$PACKAGES"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading $PACKAGES to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||||
66
.github/workflows/update-vendor-hash.yml
vendored
Normal file
66
.github/workflows/update-vendor-hash.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: Update Vendor Hash
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "core/go.mod"
|
||||||
|
- "core/go.sum"
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-vendor-hash:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
|
- name: Update vendorHash in flake.nix
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "Attempting nix build to get new vendorHash..."
|
||||||
|
if output=$(nix build .#dmsCli 2>&1); then
|
||||||
|
echo "Build succeeded, no hash update needed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
||||||
|
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||||
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
|
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||||
|
echo "Verifying build with new vendorHash..."
|
||||||
|
nix build .#dmsCli
|
||||||
|
echo "vendorHash updated successfully!"
|
||||||
|
|
||||||
|
- name: Commit and push vendorHash update
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if ! git diff --quiet flake.nix; then
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
git add flake.nix
|
||||||
|
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||||
|
git pull --rebase origin master
|
||||||
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||||
|
else
|
||||||
|
echo "No changes to flake.nix"
|
||||||
|
fi
|
||||||
44
.gitignore
vendored
44
.gitignore
vendored
@@ -27,7 +27,6 @@ qrc_*.cpp
|
|||||||
ui_*.h
|
ui_*.h
|
||||||
*.qmlc
|
*.qmlc
|
||||||
*.jsc
|
*.jsc
|
||||||
Makefile*
|
|
||||||
*build-*
|
*build-*
|
||||||
*.qm
|
*.qm
|
||||||
*.prl
|
*.prl
|
||||||
@@ -101,4 +100,45 @@ 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/
|
||||||
|
|
||||||
|
# Extracted source trees in Ubuntu package directories
|
||||||
|
distro/ubuntu/*/dms-git-repo/
|
||||||
|
distro/ubuntu/*/DankMaterialShell-*/
|
||||||
|
distro/ubuntu/danklinux/*/dsearch-*/
|
||||||
|
distro/ubuntu/danklinux/*/dgop-*/
|
||||||
|
|||||||
@@ -2,28 +2,50 @@
|
|||||||
|
|
||||||
Contributions are welcome and encouraged.
|
Contributions are welcome and encouraged.
|
||||||
|
|
||||||
## Formatting
|
To contribute fork this repository, make your changes, and open a pull request.
|
||||||
|
|
||||||
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported.
|
## Setup
|
||||||
|
|
||||||
We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
|
Enable pre-commit hooks to catch CI failures before pushing:
|
||||||
|
|
||||||
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
|
```bash
|
||||||
|
git config core.hooksPath .githooks
|
||||||
```json
|
|
||||||
"customLocalFormatters.formatters": [
|
|
||||||
{
|
|
||||||
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
|
|
||||||
"languages": ["qml"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"[qml]": {
|
|
||||||
"editor.defaultFormatter": "jkillian.custom-local-formatters",
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
|
## VSCode Setup
|
||||||
|
|
||||||
|
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
||||||
|
|
||||||
|
### QML (`quickshell` directory)
|
||||||
|
|
||||||
|
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
|
||||||
|
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"qt-qml.doNotAskForQmllsDownload": true,
|
||||||
|
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create empty `.qmlls.ini` file in `quickshell/` directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd quickshell
|
||||||
|
touch .qmlls.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
|
### GO (`core` directory)
|
||||||
|
|
||||||
|
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||||
|
2. Ensure code is formatted with `make fmt`
|
||||||
|
3. Add appropriate test coverage and ensure tests pass with `make test`
|
||||||
|
4. Run `go mod tidy`
|
||||||
|
5. Open pull request
|
||||||
|
|
||||||
## Pull request
|
## Pull request
|
||||||
|
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property int cacheConfigVersion: 1
|
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
|
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
|
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
|
||||||
|
|
||||||
property bool _loading: false
|
|
||||||
|
|
||||||
property string wallpaperLastPath: ""
|
|
||||||
property string profileLastPath: ""
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
loadCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCache() {
|
|
||||||
_loading = true
|
|
||||||
parseCache(cacheFile.text())
|
|
||||||
_loading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCache(content) {
|
|
||||||
_loading = true
|
|
||||||
try {
|
|
||||||
if (content && content.trim()) {
|
|
||||||
const cache = JSON.parse(content)
|
|
||||||
|
|
||||||
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""
|
|
||||||
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""
|
|
||||||
|
|
||||||
if (cache.configVersion === undefined) {
|
|
||||||
migrateFromUndefinedToV1(cache)
|
|
||||||
cleanupUnusedKeys()
|
|
||||||
saveCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("CacheData: Failed to parse cache:", e.message)
|
|
||||||
} finally {
|
|
||||||
_loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCache() {
|
|
||||||
if (_loading)
|
|
||||||
return
|
|
||||||
cacheFile.setText(JSON.stringify({
|
|
||||||
"wallpaperLastPath": wallpaperLastPath,
|
|
||||||
"profileLastPath": profileLastPath,
|
|
||||||
"configVersion": cacheConfigVersion
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
function migrateFromUndefinedToV1(cache) {
|
|
||||||
console.log("CacheData: Migrating configuration from undefined to version 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupUnusedKeys() {
|
|
||||||
const validKeys = [
|
|
||||||
"wallpaperLastPath",
|
|
||||||
"profileLastPath",
|
|
||||||
"configVersion"
|
|
||||||
]
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = cacheFile.text()
|
|
||||||
if (!content || !content.trim()) return
|
|
||||||
|
|
||||||
const cache = JSON.parse(content)
|
|
||||||
let needsSave = false
|
|
||||||
|
|
||||||
for (const key in cache) {
|
|
||||||
if (!validKeys.includes(key)) {
|
|
||||||
console.log("CacheData: Removing unused key:", key)
|
|
||||||
delete cache[key]
|
|
||||||
needsSave = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsSave) {
|
|
||||||
cacheFile.setText(JSON.stringify(cache, null, 2))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("CacheData: Failed to cleanup unused keys:", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: cacheFile
|
|
||||||
|
|
||||||
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/cache.json"
|
|
||||||
blockLoading: true
|
|
||||||
blockWrites: true
|
|
||||||
atomicWrites: true
|
|
||||||
watchChanges: !isGreeterMode
|
|
||||||
onLoaded: {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
parseCache(cacheFile.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onLoadFailed: error => {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
console.log("CacheData: No cache file found, starting fresh")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
113
Common/I18n.qml
113
Common/I18n.qml
@@ -1,113 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Qt.labs.folderlistmodel
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property string _rawLocale: Qt.locale().name
|
|
||||||
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
|
|
||||||
readonly property var _candidates: {
|
|
||||||
const fullUnderscore = _rawLocale;
|
|
||||||
const fullHyphen = _rawLocale.replace("_", "-");
|
|
||||||
return [fullUnderscore, fullHyphen, _lang].filter(c => c && c !== "en");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
|
||||||
|
|
||||||
property string currentLocale: "en"
|
|
||||||
property var translations: ({})
|
|
||||||
property bool translationsLoaded: false
|
|
||||||
|
|
||||||
property url _selectedPath: ""
|
|
||||||
|
|
||||||
FolderListModel {
|
|
||||||
id: dir
|
|
||||||
folder: root.translationsFolder
|
|
||||||
nameFilters: ["*.json"]
|
|
||||||
showDirs: false
|
|
||||||
showDotAndDotDot: false
|
|
||||||
|
|
||||||
onStatusChanged: if (status === FolderListModel.Ready) root._pickTranslation()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: translationLoader
|
|
||||||
path: root._selectedPath
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
try {
|
|
||||||
root.translations = JSON.parse(text())
|
|
||||||
root.translationsLoaded = true
|
|
||||||
console.log(`I18n: Loaded translations for '${root.currentLocale}' ` +
|
|
||||||
`(${Object.keys(root.translations).length} contexts)`)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`I18n: Error parsing '${root.currentLocale}':`, e,
|
|
||||||
"- falling back to English")
|
|
||||||
root._fallbackToEnglish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadFailed: (error) => {
|
|
||||||
console.warn(`I18n: Failed to load '${root.currentLocale}' (${error}), ` +
|
|
||||||
"falling back to English")
|
|
||||||
root._fallbackToEnglish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _pickTranslation() {
|
|
||||||
const present = new Set()
|
|
||||||
for (let i = 0; i < dir.count; i++) {
|
|
||||||
const name = dir.get(i, "fileName") // e.g. "zh_CN.json"
|
|
||||||
if (name && name.endsWith(".json")) {
|
|
||||||
present.add(name.slice(0, -5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < _candidates.length; i++) {
|
|
||||||
const cand = _candidates[i]
|
|
||||||
if (present.has(cand)) {
|
|
||||||
_useLocale(cand, dir.folder + "/" + cand + ".json")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_fallbackToEnglish()
|
|
||||||
}
|
|
||||||
|
|
||||||
function _useLocale(localeTag, fileUrl) {
|
|
||||||
currentLocale = localeTag
|
|
||||||
_selectedPath = fileUrl
|
|
||||||
translationsLoaded = false
|
|
||||||
translations = ({})
|
|
||||||
console.log(`I18n: Using locale '${localeTag}' from ${fileUrl}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function _fallbackToEnglish() {
|
|
||||||
currentLocale = "en"
|
|
||||||
_selectedPath = ""
|
|
||||||
translationsLoaded = false
|
|
||||||
translations = ({})
|
|
||||||
console.warn("I18n: Falling back to built-in English strings")
|
|
||||||
}
|
|
||||||
|
|
||||||
function tr(term, context) {
|
|
||||||
if (!translationsLoaded || !translations) return term
|
|
||||||
const ctx = context || term
|
|
||||||
if (translations[ctx] && translations[ctx][term]) return translations[ctx][term]
|
|
||||||
for (const c in translations) {
|
|
||||||
if (translations[c] && translations[c][term]) return translations[c][term]
|
|
||||||
}
|
|
||||||
return term
|
|
||||||
}
|
|
||||||
|
|
||||||
function trContext(context, term) {
|
|
||||||
if (!translationsLoaded || !translations) return term
|
|
||||||
if (translations[context] && translations[context][term]) return translations[context][term]
|
|
||||||
return term
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import QtCore
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property url home: StandardPaths.standardLocations(
|
|
||||||
StandardPaths.HomeLocation)[0]
|
|
||||||
readonly property url pictures: StandardPaths.standardLocations(
|
|
||||||
StandardPaths.PicturesLocation)[0]
|
|
||||||
|
|
||||||
readonly property url data: `${StandardPaths.standardLocations(
|
|
||||||
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
|
||||||
readonly property url state: `${StandardPaths.standardLocations(
|
|
||||||
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
|
||||||
readonly property url cache: `${StandardPaths.standardLocations(
|
|
||||||
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
|
|
||||||
readonly property url config: `${StandardPaths.standardLocations(
|
|
||||||
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
|
|
||||||
|
|
||||||
readonly property url imagecache: `${cache}/imagecache`
|
|
||||||
|
|
||||||
function stringify(path: url): string {
|
|
||||||
return path.toString().replace(/%20/g, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
function expandTilde(path: string): string {
|
|
||||||
return strip(path.replace("~", stringify(root.home)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function shortenHome(path: string): string {
|
|
||||||
return path.replace(strip(root.home), "~")
|
|
||||||
}
|
|
||||||
|
|
||||||
function strip(path: url): string {
|
|
||||||
return stringify(path).replace("file://", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
function toFileUrl(path: string): string {
|
|
||||||
return path.startsWith("file://") ? path : "file://" + path
|
|
||||||
}
|
|
||||||
|
|
||||||
function mkdir(path: url): void {
|
|
||||||
Quickshell.execDetached(["mkdir", "-p", strip(path)])
|
|
||||||
}
|
|
||||||
|
|
||||||
function copy(from: url, to: url): void {
|
|
||||||
Quickshell.execDetached(["cp", strip(from), strip(to)])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
|
|
||||||
function moddedAppId(appId: string): string {
|
|
||||||
if (appId === "Spotify")
|
|
||||||
return "spotify-launcher"
|
|
||||||
if (appId === "beepertexts")
|
|
||||||
return "beeper"
|
|
||||||
if (appId === "home assistant desktop")
|
|
||||||
return "homeassistant-desktop"
|
|
||||||
if (appId.includes("com.transmissionbt.transmission"))
|
|
||||||
return "transmission-gtk"
|
|
||||||
return appId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int defaultDebounceMs: 50
|
|
||||||
property var _procDebouncers: ({}) // id -> { timer, command, callback, waitMs }
|
|
||||||
|
|
||||||
function runCommand(id, command, callback, debounceMs) {
|
|
||||||
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
|
|
||||||
let procId = id ? id : Math.random()
|
|
||||||
|
|
||||||
if (!_procDebouncers[procId]) {
|
|
||||||
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
|
|
||||||
t.triggered.connect(function() { _launchProc(procId) })
|
|
||||||
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait }
|
|
||||||
} else {
|
|
||||||
_procDebouncers[procId].command = command
|
|
||||||
_procDebouncers[procId].callback = callback
|
|
||||||
_procDebouncers[procId].waitMs = wait
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = _procDebouncers[procId]
|
|
||||||
entry.timer.interval = entry.waitMs
|
|
||||||
entry.timer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function _launchProc(id) {
|
|
||||||
const entry = _procDebouncers[id]
|
|
||||||
if (!entry) return
|
|
||||||
|
|
||||||
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root)
|
|
||||||
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
|
|
||||||
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
|
|
||||||
|
|
||||||
proc.stdout = out
|
|
||||||
proc.stderr = err
|
|
||||||
proc.command = entry.command
|
|
||||||
|
|
||||||
let capturedOut = ""
|
|
||||||
let exitSeen = false
|
|
||||||
let exitCodeValue = -1
|
|
||||||
|
|
||||||
out.streamFinished.connect(function() {
|
|
||||||
capturedOut = out.text || ""
|
|
||||||
maybeComplete()
|
|
||||||
})
|
|
||||||
|
|
||||||
proc.exited.connect(function(code) {
|
|
||||||
exitSeen = true
|
|
||||||
exitCodeValue = code
|
|
||||||
maybeComplete()
|
|
||||||
})
|
|
||||||
|
|
||||||
function maybeComplete() {
|
|
||||||
if (!exitSeen) return
|
|
||||||
if (typeof entry.callback === "function") {
|
|
||||||
try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) }
|
|
||||||
}
|
|
||||||
try { proc.destroy() } catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
proc.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,852 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property int sessionConfigVersion: 1
|
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
property bool hasTriedDefaultSession: false
|
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
|
||||||
|
|
||||||
property bool isLightMode: false
|
|
||||||
property bool doNotDisturb: false
|
|
||||||
|
|
||||||
property string wallpaperPath: ""
|
|
||||||
property bool perMonitorWallpaper: false
|
|
||||||
property var monitorWallpapers: ({})
|
|
||||||
property bool perModeWallpaper: false
|
|
||||||
property string wallpaperPathLight: ""
|
|
||||||
property string wallpaperPathDark: ""
|
|
||||||
property var monitorWallpapersLight: ({})
|
|
||||||
property var monitorWallpapersDark: ({})
|
|
||||||
property string wallpaperTransition: "fade"
|
|
||||||
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
|
|
||||||
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
|
|
||||||
|
|
||||||
property bool wallpaperCyclingEnabled: false
|
|
||||||
property string wallpaperCyclingMode: "interval"
|
|
||||||
property int wallpaperCyclingInterval: 300
|
|
||||||
property string wallpaperCyclingTime: "06:00"
|
|
||||||
property var monitorCyclingSettings: ({})
|
|
||||||
|
|
||||||
property bool nightModeEnabled: false
|
|
||||||
property int nightModeTemperature: 4500
|
|
||||||
property bool nightModeAutoEnabled: false
|
|
||||||
property string nightModeAutoMode: "time"
|
|
||||||
property int nightModeStartHour: 18
|
|
||||||
property int nightModeStartMinute: 0
|
|
||||||
property int nightModeEndHour: 6
|
|
||||||
property int nightModeEndMinute: 0
|
|
||||||
property real latitude: 0.0
|
|
||||||
property real longitude: 0.0
|
|
||||||
property bool nightModeUseIPLocation: false
|
|
||||||
property string nightModeLocationProvider: ""
|
|
||||||
|
|
||||||
property var pinnedApps: []
|
|
||||||
property var recentColors: []
|
|
||||||
property bool showThirdPartyPlugins: false
|
|
||||||
property string launchPrefix: ""
|
|
||||||
property string lastBrightnessDevice: ""
|
|
||||||
|
|
||||||
property int selectedGpuIndex: 0
|
|
||||||
property bool nvidiaGpuTempEnabled: false
|
|
||||||
property bool nonNvidiaGpuTempEnabled: false
|
|
||||||
property var enabledGpuPciIds: []
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
loadSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSettings() {
|
|
||||||
if (isGreeterMode) {
|
|
||||||
parseSettings(greeterSessionFile.text())
|
|
||||||
} else {
|
|
||||||
parseSettings(settingsFile.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSettings(content) {
|
|
||||||
try {
|
|
||||||
if (content && content.trim()) {
|
|
||||||
var settings = JSON.parse(content)
|
|
||||||
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
|
|
||||||
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
|
|
||||||
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
|
|
||||||
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
|
|
||||||
perModeWallpaper = settings.perModeWallpaper !== undefined ? settings.perModeWallpaper : false
|
|
||||||
wallpaperPathLight = settings.wallpaperPathLight !== undefined ? settings.wallpaperPathLight : ""
|
|
||||||
wallpaperPathDark = settings.wallpaperPathDark !== undefined ? settings.wallpaperPathDark : ""
|
|
||||||
monitorWallpapersLight = settings.monitorWallpapersLight !== undefined ? settings.monitorWallpapersLight : {}
|
|
||||||
monitorWallpapersDark = settings.monitorWallpapersDark !== undefined ? settings.monitorWallpapersDark : {}
|
|
||||||
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
|
|
||||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
|
|
||||||
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
|
|
||||||
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
|
|
||||||
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
|
|
||||||
if (settings.nightModeStartTime !== undefined) {
|
|
||||||
const parts = settings.nightModeStartTime.split(":")
|
|
||||||
nightModeStartHour = parseInt(parts[0]) || 18
|
|
||||||
nightModeStartMinute = parseInt(parts[1]) || 0
|
|
||||||
} else {
|
|
||||||
nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18
|
|
||||||
nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0
|
|
||||||
}
|
|
||||||
if (settings.nightModeEndTime !== undefined) {
|
|
||||||
const parts = settings.nightModeEndTime.split(":")
|
|
||||||
nightModeEndHour = parseInt(parts[0]) || 6
|
|
||||||
nightModeEndMinute = parseInt(parts[1]) || 0
|
|
||||||
} else {
|
|
||||||
nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6
|
|
||||||
nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0
|
|
||||||
}
|
|
||||||
latitude = settings.latitude !== undefined ? settings.latitude : 0.0
|
|
||||||
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
|
|
||||||
nightModeUseIPLocation = settings.nightModeUseIPLocation !== undefined ? settings.nightModeUseIPLocation : false
|
|
||||||
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
|
|
||||||
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
|
|
||||||
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
|
||||||
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
|
|
||||||
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
|
||||||
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
|
|
||||||
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
|
|
||||||
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
|
|
||||||
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
|
|
||||||
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
|
|
||||||
monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {}
|
|
||||||
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
|
|
||||||
launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""
|
|
||||||
wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"
|
|
||||||
includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none")
|
|
||||||
recentColors = settings.recentColors !== undefined ? settings.recentColors : []
|
|
||||||
showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false
|
|
||||||
|
|
||||||
if (settings.configVersion === undefined) {
|
|
||||||
migrateFromUndefinedToV1(settings)
|
|
||||||
saveSettings()
|
|
||||||
} else if (settings.configVersion === sessionConfigVersion) {
|
|
||||||
cleanupUnusedKeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSettings() {
|
|
||||||
if (isGreeterMode) return
|
|
||||||
settingsFile.setText(JSON.stringify({
|
|
||||||
"isLightMode": isLightMode,
|
|
||||||
"wallpaperPath": wallpaperPath,
|
|
||||||
"perMonitorWallpaper": perMonitorWallpaper,
|
|
||||||
"monitorWallpapers": monitorWallpapers,
|
|
||||||
"perModeWallpaper": perModeWallpaper,
|
|
||||||
"wallpaperPathLight": wallpaperPathLight,
|
|
||||||
"wallpaperPathDark": wallpaperPathDark,
|
|
||||||
"monitorWallpapersLight": monitorWallpapersLight,
|
|
||||||
"monitorWallpapersDark": monitorWallpapersDark,
|
|
||||||
"doNotDisturb": doNotDisturb,
|
|
||||||
"nightModeEnabled": nightModeEnabled,
|
|
||||||
"nightModeTemperature": nightModeTemperature,
|
|
||||||
"nightModeAutoEnabled": nightModeAutoEnabled,
|
|
||||||
"nightModeAutoMode": nightModeAutoMode,
|
|
||||||
"nightModeStartHour": nightModeStartHour,
|
|
||||||
"nightModeStartMinute": nightModeStartMinute,
|
|
||||||
"nightModeEndHour": nightModeEndHour,
|
|
||||||
"nightModeEndMinute": nightModeEndMinute,
|
|
||||||
"latitude": latitude,
|
|
||||||
"longitude": longitude,
|
|
||||||
"nightModeUseIPLocation": nightModeUseIPLocation,
|
|
||||||
"nightModeLocationProvider": nightModeLocationProvider,
|
|
||||||
"pinnedApps": pinnedApps,
|
|
||||||
"selectedGpuIndex": selectedGpuIndex,
|
|
||||||
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
|
|
||||||
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
|
|
||||||
"enabledGpuPciIds": enabledGpuPciIds,
|
|
||||||
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
|
|
||||||
"wallpaperCyclingMode": wallpaperCyclingMode,
|
|
||||||
"wallpaperCyclingInterval": wallpaperCyclingInterval,
|
|
||||||
"wallpaperCyclingTime": wallpaperCyclingTime,
|
|
||||||
"monitorCyclingSettings": monitorCyclingSettings,
|
|
||||||
"lastBrightnessDevice": lastBrightnessDevice,
|
|
||||||
"launchPrefix": launchPrefix,
|
|
||||||
"wallpaperTransition": wallpaperTransition,
|
|
||||||
"includedTransitions": includedTransitions,
|
|
||||||
"recentColors": recentColors,
|
|
||||||
"showThirdPartyPlugins": showThirdPartyPlugins,
|
|
||||||
"configVersion": sessionConfigVersion
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
function migrateFromUndefinedToV1(settings) {
|
|
||||||
console.log("SessionData: Migrating configuration from undefined to version 1")
|
|
||||||
if (typeof SettingsData !== "undefined") {
|
|
||||||
if (settings.acMonitorTimeout !== undefined) {
|
|
||||||
SettingsData.setAcMonitorTimeout(settings.acMonitorTimeout)
|
|
||||||
}
|
|
||||||
if (settings.acLockTimeout !== undefined) {
|
|
||||||
SettingsData.setAcLockTimeout(settings.acLockTimeout)
|
|
||||||
}
|
|
||||||
if (settings.acSuspendTimeout !== undefined) {
|
|
||||||
SettingsData.setAcSuspendTimeout(settings.acSuspendTimeout)
|
|
||||||
}
|
|
||||||
if (settings.acHibernateTimeout !== undefined) {
|
|
||||||
SettingsData.setAcHibernateTimeout(settings.acHibernateTimeout)
|
|
||||||
}
|
|
||||||
if (settings.batteryMonitorTimeout !== undefined) {
|
|
||||||
SettingsData.setBatteryMonitorTimeout(settings.batteryMonitorTimeout)
|
|
||||||
}
|
|
||||||
if (settings.batteryLockTimeout !== undefined) {
|
|
||||||
SettingsData.setBatteryLockTimeout(settings.batteryLockTimeout)
|
|
||||||
}
|
|
||||||
if (settings.batterySuspendTimeout !== undefined) {
|
|
||||||
SettingsData.setBatterySuspendTimeout(settings.batterySuspendTimeout)
|
|
||||||
}
|
|
||||||
if (settings.batteryHibernateTimeout !== undefined) {
|
|
||||||
SettingsData.setBatteryHibernateTimeout(settings.batteryHibernateTimeout)
|
|
||||||
}
|
|
||||||
if (settings.lockBeforeSuspend !== undefined) {
|
|
||||||
SettingsData.setLockBeforeSuspend(settings.lockBeforeSuspend)
|
|
||||||
}
|
|
||||||
if (settings.loginctlLockIntegration !== undefined) {
|
|
||||||
SettingsData.setLoginctlLockIntegration(settings.loginctlLockIntegration)
|
|
||||||
}
|
|
||||||
if (settings.launchPrefix !== undefined) {
|
|
||||||
SettingsData.setLaunchPrefix(settings.launchPrefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof CacheData !== "undefined") {
|
|
||||||
if (settings.wallpaperLastPath !== undefined) {
|
|
||||||
CacheData.wallpaperLastPath = settings.wallpaperLastPath
|
|
||||||
}
|
|
||||||
if (settings.profileLastPath !== undefined) {
|
|
||||||
CacheData.profileLastPath = settings.profileLastPath
|
|
||||||
}
|
|
||||||
CacheData.saveCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupUnusedKeys() {
|
|
||||||
const validKeys = [
|
|
||||||
"isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper",
|
|
||||||
"wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight",
|
|
||||||
"monitorWallpapersDark", "doNotDisturb", "nightModeEnabled",
|
|
||||||
"nightModeTemperature", "nightModeAutoEnabled", "nightModeAutoMode",
|
|
||||||
"nightModeStartHour", "nightModeStartMinute", "nightModeEndHour",
|
|
||||||
"nightModeEndMinute", "latitude", "longitude", "nightModeUseIPLocation", "nightModeLocationProvider",
|
|
||||||
"pinnedApps", "selectedGpuIndex", "nvidiaGpuTempEnabled",
|
|
||||||
"nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled",
|
|
||||||
"wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime",
|
|
||||||
"monitorCyclingSettings", "lastBrightnessDevice", "launchPrefix", "wallpaperTransition",
|
|
||||||
"includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"
|
|
||||||
]
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = settingsFile.text()
|
|
||||||
if (!content || !content.trim()) return
|
|
||||||
|
|
||||||
const settings = JSON.parse(content)
|
|
||||||
let needsSave = false
|
|
||||||
|
|
||||||
for (const key in settings) {
|
|
||||||
if (!validKeys.includes(key)) {
|
|
||||||
console.log("SessionData: Removing unused key:", key)
|
|
||||||
delete settings[key]
|
|
||||||
needsSave = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsSave) {
|
|
||||||
settingsFile.setText(JSON.stringify(settings, null, 2))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("SessionData: Failed to cleanup unused keys:", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLightMode(lightMode) {
|
|
||||||
isLightMode = lightMode
|
|
||||||
syncWallpaperForCurrentMode()
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDoNotDisturb(enabled) {
|
|
||||||
doNotDisturb = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperPath(path) {
|
|
||||||
wallpaperPath = path
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaper(imagePath) {
|
|
||||||
wallpaperPath = imagePath
|
|
||||||
if (perModeWallpaper) {
|
|
||||||
if (isLightMode) {
|
|
||||||
wallpaperPathLight = imagePath
|
|
||||||
} else {
|
|
||||||
wallpaperPathDark = imagePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperColor(color) {
|
|
||||||
wallpaperPath = color
|
|
||||||
if (perModeWallpaper) {
|
|
||||||
if (isLightMode) {
|
|
||||||
wallpaperPathLight = color
|
|
||||||
} else {
|
|
||||||
wallpaperPathDark = color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearWallpaper() {
|
|
||||||
wallpaperPath = ""
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
|
|
||||||
Theme.switchTheme(SettingsData.theme)
|
|
||||||
} else {
|
|
||||||
Theme.switchTheme("blue")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPerMonitorWallpaper(enabled) {
|
|
||||||
perMonitorWallpaper = enabled
|
|
||||||
if (enabled && perModeWallpaper) {
|
|
||||||
syncWallpaperForCurrentMode()
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPerModeWallpaper(enabled) {
|
|
||||||
if (enabled && wallpaperCyclingEnabled) {
|
|
||||||
setWallpaperCyclingEnabled(false)
|
|
||||||
}
|
|
||||||
if (enabled && perMonitorWallpaper) {
|
|
||||||
var monitorCyclingAny = false
|
|
||||||
for (var key in monitorCyclingSettings) {
|
|
||||||
if (monitorCyclingSettings[key].enabled) {
|
|
||||||
monitorCyclingAny = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (monitorCyclingAny) {
|
|
||||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
|
||||||
for (var screenName in newSettings) {
|
|
||||||
newSettings[screenName].enabled = false
|
|
||||||
}
|
|
||||||
monitorCyclingSettings = newSettings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
perModeWallpaper = enabled
|
|
||||||
if (enabled) {
|
|
||||||
if (perMonitorWallpaper) {
|
|
||||||
monitorWallpapersLight = Object.assign({}, monitorWallpapers)
|
|
||||||
monitorWallpapersDark = Object.assign({}, monitorWallpapers)
|
|
||||||
} else {
|
|
||||||
wallpaperPathLight = wallpaperPath
|
|
||||||
wallpaperPathDark = wallpaperPath
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
syncWallpaperForCurrentMode()
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMonitorWallpaper(screenName, path) {
|
|
||||||
var newMonitorWallpapers = Object.assign({}, monitorWallpapers)
|
|
||||||
if (path && path !== "") {
|
|
||||||
newMonitorWallpapers[screenName] = path
|
|
||||||
} else {
|
|
||||||
delete newMonitorWallpapers[screenName]
|
|
||||||
}
|
|
||||||
monitorWallpapers = newMonitorWallpapers
|
|
||||||
|
|
||||||
if (perModeWallpaper) {
|
|
||||||
if (isLightMode) {
|
|
||||||
var newLight = Object.assign({}, monitorWallpapersLight)
|
|
||||||
if (path && path !== "") {
|
|
||||||
newLight[screenName] = path
|
|
||||||
} else {
|
|
||||||
delete newLight[screenName]
|
|
||||||
}
|
|
||||||
monitorWallpapersLight = newLight
|
|
||||||
} else {
|
|
||||||
var newDark = Object.assign({}, monitorWallpapersDark)
|
|
||||||
if (path && path !== "") {
|
|
||||||
newDark[screenName] = path
|
|
||||||
} else {
|
|
||||||
delete newDark[screenName]
|
|
||||||
}
|
|
||||||
monitorWallpapersDark = newDark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined") {
|
|
||||||
var screens = Quickshell.screens
|
|
||||||
if (screens.length > 0 && screenName === screens[0].name) {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperTransition(transition) {
|
|
||||||
wallpaperTransition = transition
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperCyclingEnabled(enabled) {
|
|
||||||
wallpaperCyclingEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperCyclingMode(mode) {
|
|
||||||
wallpaperCyclingMode = mode
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperCyclingInterval(interval) {
|
|
||||||
wallpaperCyclingInterval = interval
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperCyclingTime(time) {
|
|
||||||
wallpaperCyclingTime = time
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMonitorCyclingEnabled(screenName, enabled) {
|
|
||||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
|
||||||
if (!newSettings[screenName]) {
|
|
||||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
|
||||||
}
|
|
||||||
newSettings[screenName].enabled = enabled
|
|
||||||
monitorCyclingSettings = newSettings
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMonitorCyclingMode(screenName, mode) {
|
|
||||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
|
||||||
if (!newSettings[screenName]) {
|
|
||||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
|
||||||
}
|
|
||||||
newSettings[screenName].mode = mode
|
|
||||||
monitorCyclingSettings = newSettings
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMonitorCyclingInterval(screenName, interval) {
|
|
||||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
|
||||||
if (!newSettings[screenName]) {
|
|
||||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
|
||||||
}
|
|
||||||
newSettings[screenName].interval = interval
|
|
||||||
monitorCyclingSettings = newSettings
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMonitorCyclingTime(screenName, time) {
|
|
||||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
|
||||||
if (!newSettings[screenName]) {
|
|
||||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
|
||||||
}
|
|
||||||
newSettings[screenName].time = time
|
|
||||||
monitorCyclingSettings = newSettings
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeEnabled(enabled) {
|
|
||||||
nightModeEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeTemperature(temperature) {
|
|
||||||
nightModeTemperature = temperature
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeAutoEnabled(enabled) {
|
|
||||||
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
|
|
||||||
nightModeAutoEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeAutoMode(mode) {
|
|
||||||
nightModeAutoMode = mode
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeStartHour(hour) {
|
|
||||||
nightModeStartHour = hour
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeStartMinute(minute) {
|
|
||||||
nightModeStartMinute = minute
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeEndHour(hour) {
|
|
||||||
nightModeEndHour = hour
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeEndMinute(minute) {
|
|
||||||
nightModeEndMinute = minute
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeUseIPLocation(use) {
|
|
||||||
nightModeUseIPLocation = use
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLatitude(lat) {
|
|
||||||
console.log("SessionData: Setting latitude to", lat)
|
|
||||||
latitude = lat
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLongitude(lng) {
|
|
||||||
console.log("SessionData: Setting longitude to", lng)
|
|
||||||
longitude = lng
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeLocationProvider(provider) {
|
|
||||||
nightModeLocationProvider = provider
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPinnedApps(apps) {
|
|
||||||
pinnedApps = apps
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPinnedApp(appId) {
|
|
||||||
if (!appId)
|
|
||||||
return
|
|
||||||
var currentPinned = [...pinnedApps]
|
|
||||||
if (currentPinned.indexOf(appId) === -1) {
|
|
||||||
currentPinned.push(appId)
|
|
||||||
setPinnedApps(currentPinned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePinnedApp(appId) {
|
|
||||||
if (!appId)
|
|
||||||
return
|
|
||||||
var currentPinned = pinnedApps.filter(id => id !== appId)
|
|
||||||
setPinnedApps(currentPinned)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPinnedApp(appId) {
|
|
||||||
return appId && pinnedApps.indexOf(appId) !== -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRecentColor(color) {
|
|
||||||
const colorStr = color.toString()
|
|
||||||
let recent = recentColors.slice()
|
|
||||||
recent = recent.filter(c => c !== colorStr)
|
|
||||||
recent.unshift(colorStr)
|
|
||||||
if (recent.length > 5) recent = recent.slice(0, 5)
|
|
||||||
recentColors = recent
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setShowThirdPartyPlugins(enabled) {
|
|
||||||
showThirdPartyPlugins = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLaunchPrefix(prefix) {
|
|
||||||
launchPrefix = prefix
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastBrightnessDevice(device) {
|
|
||||||
lastBrightnessDevice = device
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSelectedGpuIndex(index) {
|
|
||||||
selectedGpuIndex = index
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNvidiaGpuTempEnabled(enabled) {
|
|
||||||
nvidiaGpuTempEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNonNvidiaGpuTempEnabled(enabled) {
|
|
||||||
nonNvidiaGpuTempEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setEnabledGpuPciIds(pciIds) {
|
|
||||||
enabledGpuPciIds = pciIds
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncWallpaperForCurrentMode() {
|
|
||||||
if (!perModeWallpaper) return
|
|
||||||
|
|
||||||
if (perMonitorWallpaper) {
|
|
||||||
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMonitorWallpaper(screenName) {
|
|
||||||
if (!perMonitorWallpaper) {
|
|
||||||
return wallpaperPath
|
|
||||||
}
|
|
||||||
return monitorWallpapers[screenName] || wallpaperPath
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMonitorCyclingSettings(screenName) {
|
|
||||||
return monitorCyclingSettings[screenName] || {
|
|
||||||
enabled: false,
|
|
||||||
mode: "interval",
|
|
||||||
interval: 300,
|
|
||||||
time: "06:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: settingsFile
|
|
||||||
|
|
||||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
|
||||||
blockLoading: isGreeterMode
|
|
||||||
blockWrites: true
|
|
||||||
watchChanges: !isGreeterMode
|
|
||||||
onLoaded: {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
parseSettings(settingsFile.text())
|
|
||||||
hasTriedDefaultSession = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onLoadFailed: error => {
|
|
||||||
if (!isGreeterMode && !hasTriedDefaultSession) {
|
|
||||||
hasTriedDefaultSession = true
|
|
||||||
defaultSessionCheckProcess.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: greeterSessionFile
|
|
||||||
|
|
||||||
path: {
|
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
|
||||||
return greetCfgDir + "/session.json"
|
|
||||||
}
|
|
||||||
preload: isGreeterMode
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: true
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: true
|
|
||||||
onLoaded: {
|
|
||||||
if (isGreeterMode) {
|
|
||||||
parseSettings(greeterSessionFile.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: defaultSessionCheckProcess
|
|
||||||
|
|
||||||
command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir
|
|
||||||
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp --no-preserve=mode \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"]
|
|
||||||
running: false
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
console.log("Copied default-session.json to session.json")
|
|
||||||
settingsFile.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "wallpaper"
|
|
||||||
|
|
||||||
function get(): string {
|
|
||||||
if (root.perMonitorWallpaper) {
|
|
||||||
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead."
|
|
||||||
}
|
|
||||||
return root.wallpaperPath || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function set(path: string): string {
|
|
||||||
if (root.perMonitorWallpaper) {
|
|
||||||
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead."
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
return "ERROR: No path provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
|
||||||
|
|
||||||
try {
|
|
||||||
root.setWallpaper(absolutePath)
|
|
||||||
return "SUCCESS: Wallpaper set to " + absolutePath
|
|
||||||
} catch (e) {
|
|
||||||
return "ERROR: Failed to set wallpaper: " + e.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear(): string {
|
|
||||||
root.setWallpaper("")
|
|
||||||
root.setPerMonitorWallpaper(false)
|
|
||||||
root.monitorWallpapers = {}
|
|
||||||
root.saveSettings()
|
|
||||||
return "SUCCESS: All wallpapers cleared"
|
|
||||||
}
|
|
||||||
|
|
||||||
function next(): string {
|
|
||||||
if (root.perMonitorWallpaper) {
|
|
||||||
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead."
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.wallpaperPath) {
|
|
||||||
return "ERROR: No wallpaper set"
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
WallpaperCyclingService.cycleNextManually()
|
|
||||||
return "SUCCESS: Cycling to next wallpaper"
|
|
||||||
} catch (e) {
|
|
||||||
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prev(): string {
|
|
||||||
if (root.perMonitorWallpaper) {
|
|
||||||
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead."
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.wallpaperPath) {
|
|
||||||
return "ERROR: No wallpaper set"
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
WallpaperCyclingService.cyclePrevManually()
|
|
||||||
return "SUCCESS: Cycling to previous wallpaper"
|
|
||||||
} catch (e) {
|
|
||||||
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFor(screenName: string): string {
|
|
||||||
if (!screenName) {
|
|
||||||
return "ERROR: No screen name provided"
|
|
||||||
}
|
|
||||||
return root.getMonitorWallpaper(screenName) || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFor(screenName: string, path: string): string {
|
|
||||||
if (!screenName) {
|
|
||||||
return "ERROR: No screen name provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
return "ERROR: No path provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!root.perMonitorWallpaper) {
|
|
||||||
root.setPerMonitorWallpaper(true)
|
|
||||||
}
|
|
||||||
root.setMonitorWallpaper(screenName, absolutePath)
|
|
||||||
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath
|
|
||||||
} catch (e) {
|
|
||||||
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextFor(screenName: string): string {
|
|
||||||
if (!screenName) {
|
|
||||||
return "ERROR: No screen name provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
|
||||||
if (!currentWallpaper) {
|
|
||||||
return "ERROR: No wallpaper set for " + screenName
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
WallpaperCyclingService.cycleNextForMonitor(screenName)
|
|
||||||
return "SUCCESS: Cycling to next wallpaper for " + screenName
|
|
||||||
} catch (e) {
|
|
||||||
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevFor(screenName: string): string {
|
|
||||||
if (!screenName) {
|
|
||||||
return "ERROR: No screen name provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
|
||||||
if (!currentWallpaper) {
|
|
||||||
return "ERROR: No wallpaper set for " + screenName
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
WallpaperCyclingService.cyclePrevForMonitor(screenName)
|
|
||||||
return "SUCCESS: Cycling to previous wallpaper for " + screenName
|
|
||||||
} catch (e) {
|
|
||||||
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
1068
Common/Theme.qml
1068
Common/Theme.qml
File diff suppressed because it is too large
Load Diff
556
DMSShell.qml
556
DMSShell.qml
@@ -1,556 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals
|
|
||||||
import qs.Modals.Clipboard
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Modals.Settings
|
|
||||||
import qs.Modals.Spotlight
|
|
||||||
import qs.Modules
|
|
||||||
import qs.Modules.AppDrawer
|
|
||||||
import qs.Modules.DankDash
|
|
||||||
import qs.Modules.ControlCenter
|
|
||||||
import qs.Modules.Dock
|
|
||||||
import qs.Modules.Lock
|
|
||||||
import qs.Modules.Notepad
|
|
||||||
import qs.Modules.Notifications.Center
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Notifications.Popup
|
|
||||||
import qs.Modules.OSD
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
import qs.Modules.Settings
|
|
||||||
import qs.Modules.DankBar
|
|
||||||
import qs.Modules.DankBar.Popouts
|
|
||||||
import qs.Modules.HyprWorkspaces
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
Instantiator {
|
|
||||||
id: daemonPluginInstantiator
|
|
||||||
asynchronous: true
|
|
||||||
model: Object.keys(PluginService.pluginDaemonComponents)
|
|
||||||
|
|
||||||
delegate: Loader {
|
|
||||||
id: daemonLoader
|
|
||||||
property string pluginId: modelData
|
|
||||||
sourceComponent: PluginService.pluginDaemonComponents[pluginId]
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (item) {
|
|
||||||
item.pluginService = PluginService
|
|
||||||
if (item.popoutService !== undefined) {
|
|
||||||
item.popoutService = PopoutService
|
|
||||||
}
|
|
||||||
item.pluginId = pluginId
|
|
||||||
console.log("Daemon plugin loaded:", pluginId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WallpaperBackground {}
|
|
||||||
|
|
||||||
Lock {
|
|
||||||
id: lock
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: dankBarLoader
|
|
||||||
asynchronous: false
|
|
||||||
|
|
||||||
property var currentPosition: SettingsData.dankBarPosition
|
|
||||||
property bool initialized: false
|
|
||||||
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
|
|
||||||
|
|
||||||
sourceComponent: DankBar {
|
|
||||||
hyprlandOverviewLoader: dankBarLoader.hyprlandOverviewLoaderRef
|
|
||||||
|
|
||||||
onColorPickerRequested: {
|
|
||||||
if (colorPickerModal.shouldBeVisible) {
|
|
||||||
colorPickerModal.close()
|
|
||||||
} else {
|
|
||||||
colorPickerModal.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentPositionChanged: {
|
|
||||||
if (!initialized)
|
|
||||||
return
|
|
||||||
|
|
||||||
const component = sourceComponent
|
|
||||||
sourceComponent = null
|
|
||||||
sourceComponent = component
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: dockLoader
|
|
||||||
active: true
|
|
||||||
asynchronous: false
|
|
||||||
|
|
||||||
property var currentPosition: SettingsData.dockPosition
|
|
||||||
property bool initialized: false
|
|
||||||
|
|
||||||
sourceComponent: Dock {
|
|
||||||
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (item) {
|
|
||||||
dockContextMenuLoader.active = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentPositionChanged: {
|
|
||||||
if (!initialized)
|
|
||||||
return
|
|
||||||
|
|
||||||
console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock")
|
|
||||||
const comp = sourceComponent
|
|
||||||
sourceComponent = null
|
|
||||||
sourceComponent = comp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: dankDashPopoutLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
DankDashPopout {
|
|
||||||
id: dankDashPopout
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.dankDashPopout = dankDashPopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: dockContextMenuLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
DockContextMenu {
|
|
||||||
id: dockContextMenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: notificationCenterLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
NotificationCenterPopout {
|
|
||||||
id: notificationCenter
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.notificationCenterPopout = notificationCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("notifications")
|
|
||||||
|
|
||||||
delegate: NotificationPopupManager {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: controlCenterLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
property var modalRef: colorPickerModal
|
|
||||||
property LazyLoader powerModalLoaderRef: powerMenuModalLoader
|
|
||||||
|
|
||||||
ControlCenterPopout {
|
|
||||||
id: controlCenterPopout
|
|
||||||
colorPickerModal: controlCenterLoader.modalRef
|
|
||||||
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
|
|
||||||
|
|
||||||
onLockRequested: {
|
|
||||||
lock.activate()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.controlCenterPopout = controlCenterPopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WifiPasswordModal {
|
|
||||||
id: wifiPasswordModal
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.wifiPasswordModal = wifiPasswordModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: NetworkService
|
|
||||||
|
|
||||||
function onCredentialsNeeded(token, ssid, setting, fields, hints, reason) {
|
|
||||||
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: networkInfoModalLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
NetworkInfoModal {
|
|
||||||
id: networkInfoModal
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.networkInfoModal = networkInfoModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: batteryPopoutLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
BatteryPopout {
|
|
||||||
id: batteryPopout
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.batteryPopout = batteryPopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: vpnPopoutLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
VpnPopout {
|
|
||||||
id: vpnPopout
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.vpnPopout = vpnPopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: powerMenuLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
PowerMenu {
|
|
||||||
id: powerMenu
|
|
||||||
|
|
||||||
onPowerActionRequested: (action, title, message) => {
|
|
||||||
if (SettingsData.powerActionConfirm) {
|
|
||||||
powerConfirmModalLoader.active = true
|
|
||||||
if (powerConfirmModalLoader.item) {
|
|
||||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
|
||||||
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
actionApply(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function actionApply(action) {
|
|
||||||
switch (action) {
|
|
||||||
case "logout":
|
|
||||||
SessionService.logout()
|
|
||||||
break
|
|
||||||
case "suspend":
|
|
||||||
SessionService.suspend()
|
|
||||||
break
|
|
||||||
case "hibernate":
|
|
||||||
SessionService.hibernate()
|
|
||||||
break
|
|
||||||
case "reboot":
|
|
||||||
SessionService.reboot()
|
|
||||||
break
|
|
||||||
case "poweroff":
|
|
||||||
SessionService.poweroff()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: powerConfirmModalLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
ConfirmModal {
|
|
||||||
id: powerConfirmModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: processListPopoutLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
ProcessListPopout {
|
|
||||||
id: processListPopout
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.processListPopout = processListPopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsModal {
|
|
||||||
id: settingsModal
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.settingsModal = settingsModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: appDrawerLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
AppDrawerPopout {
|
|
||||||
id: appDrawerPopout
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.appDrawerPopout = appDrawerPopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotlightModal {
|
|
||||||
id: spotlightModal
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.spotlightModal = spotlightModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClipboardHistoryModal {
|
|
||||||
id: clipboardHistoryModalPopup
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.clipboardHistoryModal = clipboardHistoryModalPopup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationModal {
|
|
||||||
id: notificationModal
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.notificationModal = notificationModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankColorPickerModal {
|
|
||||||
id: colorPickerModal
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.colorPickerModal = colorPickerModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: processListModalLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
ProcessListModal {
|
|
||||||
id: processListModal
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.processListModal = processListModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: systemUpdateLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
SystemUpdatePopout {
|
|
||||||
id: systemUpdatePopout
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.systemUpdatePopout = systemUpdatePopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
id: notepadSlideoutVariants
|
|
||||||
model: SettingsData.getFilteredScreens("notepad")
|
|
||||||
|
|
||||||
delegate: DankSlideout {
|
|
||||||
id: notepadSlideout
|
|
||||||
modelData: item
|
|
||||||
title: I18n.tr("Notepad")
|
|
||||||
slideoutWidth: 480
|
|
||||||
expandable: true
|
|
||||||
expandedWidthValue: 960
|
|
||||||
customTransparency: SettingsData.notepadTransparencyOverride
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Notepad {
|
|
||||||
onHideRequested: {
|
|
||||||
notepadSlideout.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (isVisible) {
|
|
||||||
hide()
|
|
||||||
} else {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: powerMenuModalLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
PowerMenuModal {
|
|
||||||
id: powerMenuModal
|
|
||||||
|
|
||||||
onPowerActionRequested: (action, title, message) => {
|
|
||||||
if (SettingsData.powerActionConfirm) {
|
|
||||||
powerConfirmModalLoader.active = true
|
|
||||||
if (powerConfirmModalLoader.item) {
|
|
||||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
|
||||||
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
actionApply(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function actionApply(action) {
|
|
||||||
switch (action) {
|
|
||||||
case "logout":
|
|
||||||
SessionService.logout()
|
|
||||||
break
|
|
||||||
case "suspend":
|
|
||||||
SessionService.suspend()
|
|
||||||
break
|
|
||||||
case "hibernate":
|
|
||||||
SessionService.hibernate()
|
|
||||||
break
|
|
||||||
case "reboot":
|
|
||||||
SessionService.reboot()
|
|
||||||
break
|
|
||||||
case "poweroff":
|
|
||||||
SessionService.poweroff()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.powerMenuModal = powerMenuModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: hyprKeybindsModalLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
HyprKeybindsModal {
|
|
||||||
id: hyprKeybindsModal
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.hyprKeybindsModal = hyprKeybindsModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DMSShellIPC {
|
|
||||||
powerMenuModalLoader: powerMenuModalLoader
|
|
||||||
processListModalLoader: processListModalLoader
|
|
||||||
controlCenterLoader: controlCenterLoader
|
|
||||||
dankDashPopoutLoader: dankDashPopoutLoader
|
|
||||||
notepadSlideoutVariants: notepadSlideoutVariants
|
|
||||||
hyprKeybindsModalLoader: hyprKeybindsModalLoader
|
|
||||||
dankBarLoader: dankBarLoader
|
|
||||||
hyprlandOverviewLoader: hyprlandOverviewLoader
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("toast")
|
|
||||||
|
|
||||||
delegate: Toast {
|
|
||||||
modelData: item
|
|
||||||
visible: ToastService.toastVisible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: VolumeOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: MicMuteOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: BrightnessOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: IdleInhibitorOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: hyprlandOverviewLoader
|
|
||||||
active: CompositorService.isHyprland
|
|
||||||
component: HyprlandOverview {
|
|
||||||
id: hyprlandOverview
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
388
DMSShellIPC.qml
388
DMSShellIPC.qml
@@ -1,388 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property var powerMenuModalLoader
|
|
||||||
required property var processListModalLoader
|
|
||||||
required property var controlCenterLoader
|
|
||||||
required property var dankDashPopoutLoader
|
|
||||||
required property var notepadSlideoutVariants
|
|
||||||
required property var hyprKeybindsModalLoader
|
|
||||||
required property var dankBarLoader
|
|
||||||
required property var hyprlandOverviewLoader
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open() {
|
|
||||||
root.powerMenuModalLoader.active = true
|
|
||||||
if (root.powerMenuModalLoader.item)
|
|
||||||
root.powerMenuModalLoader.item.openCentered()
|
|
||||||
|
|
||||||
return "POWERMENU_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
if (root.powerMenuModalLoader.item)
|
|
||||||
root.powerMenuModalLoader.item.close()
|
|
||||||
|
|
||||||
return "POWERMENU_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
root.powerMenuModalLoader.active = true
|
|
||||||
if (root.powerMenuModalLoader.item) {
|
|
||||||
if (root.powerMenuModalLoader.item.shouldBeVisible) {
|
|
||||||
root.powerMenuModalLoader.item.close()
|
|
||||||
} else {
|
|
||||||
root.powerMenuModalLoader.item.openCentered()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "POWERMENU_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "powermenu"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
root.processListModalLoader.active = true
|
|
||||||
if (root.processListModalLoader.item)
|
|
||||||
root.processListModalLoader.item.show()
|
|
||||||
|
|
||||||
return "PROCESSLIST_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
if (root.processListModalLoader.item)
|
|
||||||
root.processListModalLoader.item.hide()
|
|
||||||
|
|
||||||
return "PROCESSLIST_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
root.processListModalLoader.active = true
|
|
||||||
if (root.processListModalLoader.item)
|
|
||||||
root.processListModalLoader.item.toggle()
|
|
||||||
|
|
||||||
return "PROCESSLIST_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "processlist"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
if (root.dankBarLoader.item) {
|
|
||||||
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
|
|
||||||
return "CONTROL_CENTER_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
return "CONTROL_CENTER_OPEN_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
if (root.controlCenterLoader.item) {
|
|
||||||
root.controlCenterLoader.item.close()
|
|
||||||
return "CONTROL_CENTER_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "CONTROL_CENTER_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
if (root.dankBarLoader.item) {
|
|
||||||
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
|
|
||||||
return "CONTROL_CENTER_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "CONTROL_CENTER_TOGGLE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "control-center"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(tab: string): string {
|
|
||||||
root.dankDashPopoutLoader.active = true
|
|
||||||
if (root.dankDashPopoutLoader.item) {
|
|
||||||
switch (tab.toLowerCase()) {
|
|
||||||
case "media":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 1
|
|
||||||
break
|
|
||||||
case "weather":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 0
|
|
||||||
break
|
|
||||||
}
|
|
||||||
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
|
||||||
root.dankDashPopoutLoader.item.dashVisible = true
|
|
||||||
return "DASH_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
return "DASH_OPEN_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
if (root.dankDashPopoutLoader.item) {
|
|
||||||
root.dankDashPopoutLoader.item.dashVisible = false
|
|
||||||
return "DASH_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "DASH_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(tab: string): string {
|
|
||||||
root.dankDashPopoutLoader.active = true
|
|
||||||
if (root.dankDashPopoutLoader.item) {
|
|
||||||
if (root.dankDashPopoutLoader.item.dashVisible) {
|
|
||||||
root.dankDashPopoutLoader.item.dashVisible = false
|
|
||||||
} else {
|
|
||||||
switch (tab.toLowerCase()) {
|
|
||||||
case "media":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 1
|
|
||||||
break
|
|
||||||
case "weather":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 0
|
|
||||||
break
|
|
||||||
}
|
|
||||||
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
|
||||||
root.dankDashPopoutLoader.item.dashVisible = true
|
|
||||||
}
|
|
||||||
return "DASH_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "DASH_TOGGLE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "dash"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function getFocusedScreenName() {
|
|
||||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
|
||||||
return Hyprland.focusedWorkspace.monitor.name
|
|
||||||
}
|
|
||||||
if (CompositorService.isNiri && NiriService.currentOutput) {
|
|
||||||
return NiriService.currentOutput
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveNotepadInstance() {
|
|
||||||
if (root.notepadSlideoutVariants.instances.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.notepadSlideoutVariants.instances.length === 1) {
|
|
||||||
return root.notepadSlideoutVariants.instances[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
var focusedScreen = getFocusedScreenName()
|
|
||||||
if (focusedScreen && root.notepadSlideoutVariants.instances.length > 0) {
|
|
||||||
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
|
|
||||||
var slideout = root.notepadSlideoutVariants.instances[i]
|
|
||||||
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
|
|
||||||
return slideout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
|
|
||||||
var slideout = root.notepadSlideoutVariants.instances[i]
|
|
||||||
if (slideout.isVisible) {
|
|
||||||
return slideout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return root.notepadSlideoutVariants.instances[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function open(): string {
|
|
||||||
var instance = getActiveNotepadInstance()
|
|
||||||
if (instance) {
|
|
||||||
instance.show()
|
|
||||||
return "NOTEPAD_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
return "NOTEPAD_OPEN_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
var instance = getActiveNotepadInstance()
|
|
||||||
if (instance) {
|
|
||||||
instance.hide()
|
|
||||||
return "NOTEPAD_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "NOTEPAD_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
var instance = getActiveNotepadInstance()
|
|
||||||
if (instance) {
|
|
||||||
instance.toggle()
|
|
||||||
return "NOTEPAD_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "NOTEPAD_TOGGLE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "notepad"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function toggle(): string {
|
|
||||||
SessionService.toggleIdleInhibit()
|
|
||||||
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function enable(): string {
|
|
||||||
SessionService.enableIdleInhibit()
|
|
||||||
return "Idle inhibit enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function disable(): string {
|
|
||||||
SessionService.disableIdleInhibit()
|
|
||||||
return "Idle inhibit disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function status(): string {
|
|
||||||
return SessionService.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function reason(newReason: string): string {
|
|
||||||
if (!newReason) {
|
|
||||||
return `Current reason: ${SessionService.inhibitReason}`
|
|
||||||
}
|
|
||||||
|
|
||||||
SessionService.setInhibitReason(newReason)
|
|
||||||
return `Inhibit reason set to: ${newReason}`
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "inhibit"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function list(): string {
|
|
||||||
return MprisController.availablePlayers.map(p => p.identity).join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
function play(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canPlay) {
|
|
||||||
MprisController.activePlayer.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pause(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canPause) {
|
|
||||||
MprisController.activePlayer.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function playPause(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) {
|
|
||||||
MprisController.activePlayer.togglePlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previous(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
|
|
||||||
MprisController.activePlayer.previous()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function next(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
|
|
||||||
MprisController.activePlayer.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop(): void {
|
|
||||||
if (MprisController.activePlayer) {
|
|
||||||
MprisController.activePlayer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "mpris"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function openBinds(): string {
|
|
||||||
if (!CompositorService.isHyprland) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
root.hyprKeybindsModalLoader.active = true
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
root.hyprKeybindsModalLoader.item.open()
|
|
||||||
return "HYPR_KEYBINDS_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_OPEN_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeBinds(): string {
|
|
||||||
if (!CompositorService.isHyprland) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
root.hyprKeybindsModalLoader.item.close()
|
|
||||||
return "HYPR_KEYBINDS_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleBinds(): string {
|
|
||||||
if (!CompositorService.isHyprland) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
root.hyprKeybindsModalLoader.active = true
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
|
||||||
root.hyprKeybindsModalLoader.item.close()
|
|
||||||
} else {
|
|
||||||
root.hyprKeybindsModalLoader.item.open()
|
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_TOGGLE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleOverview(): string {
|
|
||||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
|
||||||
return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeOverview(): string {
|
|
||||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = false
|
|
||||||
return "OVERVIEW_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function openOverview(): string {
|
|
||||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = true
|
|
||||||
return "OVERVIEW_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "hypr"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function wallpaper(): string {
|
|
||||||
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
|
|
||||||
return "SUCCESS: Toggled wallpaper browser"
|
|
||||||
}
|
|
||||||
return "ERROR: Failed to toggle wallpaper browser"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "dankdash"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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>.
|
|
||||||
|
|||||||
156
Makefile
Normal file
156
Makefile
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Root Makefile for DankMaterialShell (DMS)
|
||||||
|
# Orchestrates building, installation, and systemd management
|
||||||
|
|
||||||
|
# Build configuration
|
||||||
|
BINARY_NAME=dms
|
||||||
|
CORE_DIR=core
|
||||||
|
BUILD_DIR=$(CORE_DIR)/bin
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
INSTALL_DIR=$(PREFIX)/bin
|
||||||
|
DATA_DIR=$(PREFIX)/share
|
||||||
|
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
||||||
|
|
||||||
|
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
||||||
|
SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user
|
||||||
|
|
||||||
|
SHELL_DIR=quickshell
|
||||||
|
SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||||
|
ASSETS_DIR=assets
|
||||||
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
|
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
@echo "Building $(BINARY_NAME)..."
|
||||||
|
@$(MAKE) -C $(CORE_DIR) build
|
||||||
|
@echo "Build complete"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
# Installation targets
|
||||||
|
install-bin:
|
||||||
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Binary installed"
|
||||||
|
|
||||||
|
install-shell:
|
||||||
|
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
||||||
|
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||||
|
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||||
|
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||||
|
@echo "Shell files installed"
|
||||||
|
|
||||||
|
install-completions:
|
||||||
|
@echo "Installing shell completions..."
|
||||||
|
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
||||||
|
@mkdir -p $(DATA_DIR)/zsh/site-functions
|
||||||
|
@mkdir -p $(DATA_DIR)/fish/vendor_completions.d
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion bash > $(DATA_DIR)/bash-completion/completions/dms 2>/dev/null || true
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion zsh > $(DATA_DIR)/zsh/site-functions/_dms 2>/dev/null || true
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion fish > $(DATA_DIR)/fish/vendor_completions.d/dms.fish 2>/dev/null || true
|
||||||
|
@echo "Shell completions installed"
|
||||||
|
|
||||||
|
install-systemd:
|
||||||
|
@echo "Installing systemd user service..."
|
||||||
|
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||||
|
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
|
||||||
|
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||||
|
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||||
|
|
||||||
|
install-icon:
|
||||||
|
@echo "Installing icon..."
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg
|
||||||
|
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||||
|
@echo "Icon installed"
|
||||||
|
|
||||||
|
install-desktop:
|
||||||
|
@echo "Installing desktop entry..."
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
|
@echo "Desktop entry installed"
|
||||||
|
|
||||||
|
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
|
@echo ""
|
||||||
|
@echo "Installation complete!"
|
||||||
|
@echo ""
|
||||||
|
@echo "To enable and start DMS:"
|
||||||
|
@echo " systemctl --user enable --now dms"
|
||||||
|
|
||||||
|
# Uninstallation targets
|
||||||
|
uninstall-bin:
|
||||||
|
@echo "Removing $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||||
|
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Binary removed"
|
||||||
|
|
||||||
|
uninstall-shell:
|
||||||
|
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
|
||||||
|
@rm -rf $(SHELL_INSTALL_DIR)
|
||||||
|
@echo "Shell files removed"
|
||||||
|
|
||||||
|
uninstall-completions:
|
||||||
|
@echo "Removing shell completions..."
|
||||||
|
@rm -f $(DATA_DIR)/bash-completion/completions/dms
|
||||||
|
@rm -f $(DATA_DIR)/zsh/site-functions/_dms
|
||||||
|
@rm -f $(DATA_DIR)/fish/vendor_completions.d/dms.fish
|
||||||
|
@echo "Shell completions removed"
|
||||||
|
|
||||||
|
uninstall-systemd:
|
||||||
|
@echo "Removing systemd user service..."
|
||||||
|
@rm -f $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@echo "Systemd service removed"
|
||||||
|
@echo "Note: Stop/disable service manually if running: systemctl --user stop dms"
|
||||||
|
|
||||||
|
uninstall-icon:
|
||||||
|
@echo "Removing icon..."
|
||||||
|
@rm -f $(ICON_DIR)/danklogo.svg
|
||||||
|
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||||
|
@echo "Icon removed"
|
||||||
|
|
||||||
|
uninstall-desktop:
|
||||||
|
@echo "Removing desktop entry..."
|
||||||
|
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
|
@echo "Desktop entry removed"
|
||||||
|
|
||||||
|
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
|
||||||
|
@echo ""
|
||||||
|
@echo "Uninstallation complete!"
|
||||||
|
|
||||||
|
# Target assist
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo ""
|
||||||
|
@echo "Build:"
|
||||||
|
@echo " all (default) - Build the DMS binary"
|
||||||
|
@echo " build - Same as 'all'"
|
||||||
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo ""
|
||||||
|
@echo "Install:"
|
||||||
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
@echo " install-bin - Install only the binary"
|
||||||
|
@echo " install-shell - Install only shell files"
|
||||||
|
@echo " install-completions - Install only shell completions"
|
||||||
|
@echo " install-systemd - Install only systemd service"
|
||||||
|
@echo " install-icon - Install only icon"
|
||||||
|
@echo " install-desktop - Install only desktop entry"
|
||||||
|
@echo ""
|
||||||
|
@echo "Uninstall:"
|
||||||
|
@echo " uninstall - Remove everything (requires sudo)"
|
||||||
|
@echo " uninstall-bin - Remove only the binary"
|
||||||
|
@echo " uninstall-shell - Remove only shell files"
|
||||||
|
@echo " uninstall-completions - Remove only shell completions"
|
||||||
|
@echo " uninstall-systemd - Remove only systemd service"
|
||||||
|
@echo " uninstall-icon - Remove only icon"
|
||||||
|
@echo " uninstall-desktop - Remove only desktop entry"
|
||||||
|
@echo ""
|
||||||
|
@echo "Usage:"
|
||||||
|
@echo " sudo make install - Build and install DMS"
|
||||||
|
@echo " sudo make uninstall - Remove DMS"
|
||||||
|
@echo " systemctl --user enable --now dms - Enable and start service"
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "quickshell:modal"
|
|
||||||
|
|
||||||
property alias content: contentLoader.sourceComponent
|
|
||||||
property alias contentLoader: contentLoader
|
|
||||||
property Item directContent: null
|
|
||||||
property real width: 400
|
|
||||||
property real height: 300
|
|
||||||
readonly property real screenWidth: screen ? screen.width : 1920
|
|
||||||
readonly property real screenHeight: screen ? screen.height : 1080
|
|
||||||
readonly property real dpr: {
|
|
||||||
if (CompositorService.isNiri && screen) {
|
|
||||||
const niriScale = NiriService.displayScales[screen.name]
|
|
||||||
if (niriScale !== undefined) return niriScale
|
|
||||||
}
|
|
||||||
if (CompositorService.isHyprland && screen) {
|
|
||||||
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
|
|
||||||
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
|
|
||||||
}
|
|
||||||
return (screen?.devicePixelRatio) || 1
|
|
||||||
}
|
|
||||||
property bool showBackground: true
|
|
||||||
property real backgroundOpacity: 0.5
|
|
||||||
property string positioning: "center"
|
|
||||||
property point customPosition: Qt.point(0, 0)
|
|
||||||
property bool closeOnEscapeKey: true
|
|
||||||
property bool closeOnBackgroundClick: true
|
|
||||||
property string animationType: "scale"
|
|
||||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
|
||||||
property real animationScaleCollapsed: 0.96
|
|
||||||
property real animationOffset: Theme.spacingL
|
|
||||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
|
||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
|
||||||
property color borderColor: Theme.outlineMedium
|
|
||||||
property real borderWidth: 1
|
|
||||||
property real cornerRadius: Theme.cornerRadius
|
|
||||||
property bool enableShadow: false
|
|
||||||
property alias modalFocusScope: focusScope
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property bool shouldHaveFocus: shouldBeVisible
|
|
||||||
property bool allowFocusOverride: false
|
|
||||||
property bool allowStacking: false
|
|
||||||
property bool keepContentLoaded: false
|
|
||||||
|
|
||||||
signal opened
|
|
||||||
signal dialogClosed
|
|
||||||
signal backgroundClicked
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
ModalManager.openModal(root)
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
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 + 120
|
|
||||||
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: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contentContainer
|
|
||||||
|
|
||||||
width: Theme.px(root.width, dpr)
|
|
||||||
height: Theme.px(root.height, dpr)
|
|
||||||
anchors.centerIn: undefined
|
|
||||||
x: {
|
|
||||||
if (positioning === "center") {
|
|
||||||
return Theme.snap((root.screenWidth - width) / 2, dpr)
|
|
||||||
} else if (positioning === "top-right") {
|
|
||||||
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
|
|
||||||
} else if (positioning === "custom") {
|
|
||||||
return Theme.snap(root.customPosition.x, dpr)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (positioning === "center") {
|
|
||||||
return Theme.snap((root.screenHeight - height) / 2, dpr)
|
|
||||||
} else if (positioning === "top-right") {
|
|
||||||
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
|
|
||||||
} else if (positioning === "custom") {
|
|
||||||
return Theme.snap(root.customPosition.y, dpr)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
color: root.backgroundColor
|
|
||||||
radius: root.cornerRadius
|
|
||||||
border.color: root.borderColor
|
|
||||||
border.width: root.borderWidth
|
|
||||||
clip: false
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
opacity: root.shouldBeVisible ? 1 : 0
|
|
||||||
transform: [scaleTransform, motionTransform]
|
|
||||||
|
|
||||||
Scale {
|
|
||||||
id: scaleTransform
|
|
||||||
|
|
||||||
origin.x: contentContainer.width / 2
|
|
||||||
origin.y: contentContainer.height / 2
|
|
||||||
xScale: root.shouldBeVisible ? 1 : root.animationScaleCollapsed
|
|
||||||
yScale: root.shouldBeVisible ? 1 : root.animationScaleCollapsed
|
|
||||||
|
|
||||||
Behavior on xScale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on yScale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Translate {
|
|
||||||
id: motionTransform
|
|
||||||
|
|
||||||
readonly property bool slide: root.animationType === "slide"
|
|
||||||
readonly property real hiddenX: slide ? 15 : 0
|
|
||||||
readonly property real hiddenY: slide ? -30 : root.animationOffset
|
|
||||||
|
|
||||||
x: Theme.snap(root.shouldBeVisible ? 0 : hiddenX, root.dpr)
|
|
||||||
y: Theme.snap(root.shouldBeVisible ? 0 : hiddenY, root.dpr)
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: root.shouldBeVisible
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: directContentWrapper
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.directContent !== null
|
|
||||||
focus: true
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.directContent) {
|
|
||||||
root.directContent.parent = directContentWrapper
|
|
||||||
root.directContent.anchors.fill = directContentWrapper
|
|
||||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onDirectContentChanged() {
|
|
||||||
if (root.directContent) {
|
|
||||||
root.directContent.parent = directContentWrapper
|
|
||||||
root.directContent.anchors.fill = directContentWrapper
|
|
||||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
|
|
||||||
asynchronous: false
|
|
||||||
focus: true
|
|
||||||
clip: false
|
|
||||||
visible: root.directContent === null
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (item) {
|
|
||||||
Qt.callLater(() => item.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: focusScope
|
|
||||||
|
|
||||||
objectName: "modalFocusScope"
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.shouldBeVisible || root.visible
|
|
||||||
focus: root.shouldBeVisible
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && shouldHaveFocus) {
|
|
||||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldHaveFocusChanged() {
|
|
||||||
if (shouldHaveFocus && shouldBeVisible) {
|
|
||||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,542 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string pickerTitle: "Choose Color"
|
|
||||||
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
|
|
||||||
property var onColorSelectedCallback: null
|
|
||||||
|
|
||||||
signal colorSelected(color selectedColor)
|
|
||||||
|
|
||||||
property color currentColor: Theme.primary
|
|
||||||
property real hue: 0
|
|
||||||
property real saturation: 1
|
|
||||||
property real value: 1
|
|
||||||
property real alpha: 1
|
|
||||||
property real gradientX: 0
|
|
||||||
property real gradientY: 0
|
|
||||||
|
|
||||||
readonly property var standardColors: [
|
|
||||||
"#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4",
|
|
||||||
"#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722",
|
|
||||||
"#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7",
|
|
||||||
"#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19",
|
|
||||||
"#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f",
|
|
||||||
"#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315",
|
|
||||||
"#ffffff", "#9e9e9e", "#212121"
|
|
||||||
]
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
currentColor = selectedColor
|
|
||||||
updateFromColor(currentColor)
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
onColorSelectedCallback = null
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
onColorSelected: (color) => {
|
|
||||||
if (onColorSelectedCallback) {
|
|
||||||
onColorSelectedCallback(color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyColorToClipboard(colorValue) {
|
|
||||||
Quickshell.execDetached(["sh", "-c", `echo "${colorValue}" | wl-copy`])
|
|
||||||
ToastService.showInfo(`Color ${colorValue} copied`)
|
|
||||||
SessionData.addRecentColor(currentColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFromColor(color) {
|
|
||||||
hue = color.hsvHue
|
|
||||||
saturation = color.hsvSaturation
|
|
||||||
value = color.hsvValue
|
|
||||||
alpha = color.a
|
|
||||||
gradientX = saturation
|
|
||||||
gradientY = 1 - value
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateColor() {
|
|
||||||
currentColor = Qt.hsva(hue, saturation, value, alpha)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateColorFromGradient(x, y) {
|
|
||||||
saturation = Math.max(0, Math.min(1, x))
|
|
||||||
value = Math.max(0, Math.min(1, 1 - y))
|
|
||||||
updateColor()
|
|
||||||
selectedColor = currentColor
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickColorFromScreen() {
|
|
||||||
hide()
|
|
||||||
Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (output, errorCode) => {
|
|
||||||
if (errorCode !== 0) {
|
|
||||||
console.warn("hyprpicker exited with code:", errorCode)
|
|
||||||
root.show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const colorStr = output.trim()
|
|
||||||
if (colorStr.length >= 7 && colorStr.startsWith('#')) {
|
|
||||||
const pickedColor = Qt.color(colorStr)
|
|
||||||
root.selectedColor = pickedColor
|
|
||||||
root.currentColor = pickedColor
|
|
||||||
root.updateFromColor(pickedColor)
|
|
||||||
copyColorToClipboard(colorStr)
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 680
|
|
||||||
height: 680
|
|
||||||
backgroundColor: Theme.surfaceContainer
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
borderColor: Theme.outlineMedium
|
|
||||||
borderWidth: 1
|
|
||||||
keepContentLoaded: true
|
|
||||||
|
|
||||||
onBackgroundClicked: hide()
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
FocusScope {
|
|
||||||
id: colorContent
|
|
||||||
|
|
||||||
property alias hexInput: hexInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
root.hide()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 90
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.pickerTitle
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Select a color from the palette or use custom sliders")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "colorize"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
root.pickColorFromScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: gradientPicker
|
|
||||||
width: parent.width - 70
|
|
||||||
height: 280
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 1
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.hsva(root.hue, 1, 1, 1)
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
gradient: Gradient {
|
|
||||||
orientation: Gradient.Horizontal
|
|
||||||
GradientStop { position: 0.0; color: "#ffffff" }
|
|
||||||
GradientStop { position: 1.0; color: "transparent" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
gradient: Gradient {
|
|
||||||
orientation: Gradient.Vertical
|
|
||||||
GradientStop { position: 0.0; color: "transparent" }
|
|
||||||
GradientStop { position: 1.0; color: "#000000" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: pickerCircle
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
radius: 8
|
|
||||||
border.color: "white"
|
|
||||||
border.width: 2
|
|
||||||
color: "transparent"
|
|
||||||
x: root.gradientX * parent.width - width / 2
|
|
||||||
y: root.gradientY * parent.height - height / 2
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - 4
|
|
||||||
height: parent.height - 4
|
|
||||||
radius: width / 2
|
|
||||||
border.color: "black"
|
|
||||||
border.width: 1
|
|
||||||
color: "transparent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.CrossCursor
|
|
||||||
onPressed: mouse => {
|
|
||||||
const x = Math.max(0, Math.min(1, mouse.x / width))
|
|
||||||
const y = Math.max(0, Math.min(1, mouse.y / height))
|
|
||||||
root.gradientX = x
|
|
||||||
root.gradientY = y
|
|
||||||
root.updateColorFromGradient(x, y)
|
|
||||||
}
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (pressed) {
|
|
||||||
const x = Math.max(0, Math.min(1, mouse.x / width))
|
|
||||||
const y = Math.max(0, Math.min(1, mouse.y / height))
|
|
||||||
root.gradientX = x
|
|
||||||
root.gradientY = y
|
|
||||||
root.updateColorFromGradient(x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: hueSlider
|
|
||||||
width: 50
|
|
||||||
height: 280
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
gradient: Gradient {
|
|
||||||
orientation: Gradient.Vertical
|
|
||||||
GradientStop { position: 0.00; color: "#ff0000" }
|
|
||||||
GradientStop { position: 0.17; color: "#ffff00" }
|
|
||||||
GradientStop { position: 0.33; color: "#00ff00" }
|
|
||||||
GradientStop { position: 0.50; color: "#00ffff" }
|
|
||||||
GradientStop { position: 0.67; color: "#0000ff" }
|
|
||||||
GradientStop { position: 0.83; color: "#ff00ff" }
|
|
||||||
GradientStop { position: 1.00; color: "#ff0000" }
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: hueIndicator
|
|
||||||
width: parent.width
|
|
||||||
height: 4
|
|
||||||
color: "white"
|
|
||||||
border.color: "black"
|
|
||||||
border.width: 1
|
|
||||||
y: root.hue * parent.height - height / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.SizeVerCursor
|
|
||||||
onPressed: mouse => {
|
|
||||||
const h = Math.max(0, Math.min(1, mouse.y / height))
|
|
||||||
root.hue = h
|
|
||||||
root.updateColor()
|
|
||||||
root.selectedColor = root.currentColor
|
|
||||||
}
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (pressed) {
|
|
||||||
const h = Math.max(0, Math.min(1, mouse.y / height))
|
|
||||||
root.hue = h
|
|
||||||
root.updateColor()
|
|
||||||
root.selectedColor = root.currentColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Material Colors")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
GridView {
|
|
||||||
width: parent.width
|
|
||||||
height: 140
|
|
||||||
cellWidth: 38
|
|
||||||
cellHeight: 38
|
|
||||||
clip: true
|
|
||||||
interactive: false
|
|
||||||
model: root.standardColors
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
color: modelData
|
|
||||||
radius: 4
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
const pickedColor = Qt.color(modelData)
|
|
||||||
root.selectedColor = pickedColor
|
|
||||||
root.currentColor = pickedColor
|
|
||||||
root.updateFromColor(pickedColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: 210
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Recent Colors")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: 5
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: 4
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
color: {
|
|
||||||
if (index < SessionData.recentColors.length) {
|
|
||||||
return SessionData.recentColors[index]
|
|
||||||
}
|
|
||||||
return Theme.surfaceContainerHigh
|
|
||||||
}
|
|
||||||
|
|
||||||
opacity: index < SessionData.recentColors.length ? 1.0 : 0.3
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: index < SessionData.recentColors.length ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: index < SessionData.recentColors.length
|
|
||||||
onClicked: () => {
|
|
||||||
if (index < SessionData.recentColors.length) {
|
|
||||||
const pickedColor = SessionData.recentColors[index]
|
|
||||||
root.selectedColor = pickedColor
|
|
||||||
root.currentColor = pickedColor
|
|
||||||
root.updateFromColor(pickedColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 330
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Opacity")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
value: Math.round(root.alpha * 100)
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
showValue: false
|
|
||||||
onSliderValueChanged: (newValue) => {
|
|
||||||
root.alpha = newValue / 100
|
|
||||||
root.updateColor()
|
|
||||||
root.selectedColor = root.currentColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: root.currentColor
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Hex:")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: hexInput
|
|
||||||
width: 120
|
|
||||||
height: 38
|
|
||||||
text: root.currentColor.toString()
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: {
|
|
||||||
if (text.length === 0) return Theme.surfaceText
|
|
||||||
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
|
||||||
return hexPattern.test(text) ? Theme.surfaceText : Theme.error
|
|
||||||
}
|
|
||||||
placeholderText: "#000000"
|
|
||||||
backgroundColor: Theme.surfaceHover
|
|
||||||
borderWidth: 1
|
|
||||||
focusedBorderWidth: 2
|
|
||||||
topPadding: Theme.spacingS
|
|
||||||
bottomPadding: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onAccepted: () => {
|
|
||||||
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
|
||||||
if (!hexPattern.test(text)) return
|
|
||||||
const color = Qt.color(text)
|
|
||||||
if (color) {
|
|
||||||
root.selectedColor = color
|
|
||||||
root.currentColor = color
|
|
||||||
root.updateFromColor(color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
width: 80
|
|
||||||
buttonHeight: 36
|
|
||||||
text: I18n.tr("Apply")
|
|
||||||
backgroundColor: Theme.primary
|
|
||||||
textColor: Theme.background
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: {
|
|
||||||
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
|
||||||
if (!hexPattern.test(hexInput.text)) return
|
|
||||||
const color = Qt.color(hexInput.text)
|
|
||||||
if (color) {
|
|
||||||
root.currentColor = color
|
|
||||||
root.updateFromColor(color)
|
|
||||||
root.selectedColor = root.currentColor
|
|
||||||
root.colorSelected(root.currentColor)
|
|
||||||
SessionData.addRecentColor(root.currentColor)
|
|
||||||
root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 460
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
width: 70
|
|
||||||
buttonHeight: 36
|
|
||||||
text: I18n.tr("Cancel")
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: root.hide()
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
width: 70
|
|
||||||
buttonHeight: 36
|
|
||||||
text: I18n.tr("Copy")
|
|
||||||
backgroundColor: Theme.primary
|
|
||||||
textColor: Theme.background
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: {
|
|
||||||
const colorString = root.currentColor.toString()
|
|
||||||
root.copyColorToClipboard(colorString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,266 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
width: 1400
|
|
||||||
height: 900
|
|
||||||
onBackgroundClicked: close()
|
|
||||||
|
|
||||||
function categorizeKeybinds() {
|
|
||||||
const categories = {
|
|
||||||
"Workspace": [],
|
|
||||||
"Window": [],
|
|
||||||
"Monitor": [],
|
|
||||||
"Execute": [],
|
|
||||||
"System": [],
|
|
||||||
"Other": []
|
|
||||||
}
|
|
||||||
|
|
||||||
function addKeybind(keybind) {
|
|
||||||
const dispatcher = keybind.dispatcher || ""
|
|
||||||
if (dispatcher.includes("workspace")) {
|
|
||||||
categories["Workspace"].push(keybind)
|
|
||||||
} else if (dispatcher.includes("monitor")) {
|
|
||||||
categories["Monitor"].push(keybind)
|
|
||||||
} else if (dispatcher.includes("window") || dispatcher.includes("focus") || dispatcher.includes("move") || dispatcher.includes("swap") || dispatcher.includes("resize") || dispatcher === "killactive" || dispatcher === "fullscreen" || dispatcher === "togglefloating") {
|
|
||||||
categories["Window"].push(keybind)
|
|
||||||
} else if (dispatcher === "exec") {
|
|
||||||
categories["Execute"].push(keybind)
|
|
||||||
} else if (dispatcher === "exit" || dispatcher.includes("dpms")) {
|
|
||||||
categories["System"].push(keybind)
|
|
||||||
} else {
|
|
||||||
categories["Other"].push(keybind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allKeybinds = HyprKeybindsService.keybinds.keybinds || []
|
|
||||||
for (let i = 0; i < allKeybinds.length; i++) {
|
|
||||||
addKeybind(allKeybinds[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
const children = HyprKeybindsService.keybinds.children || []
|
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
const child = children[i]
|
|
||||||
const childKeybinds = child.keybinds || []
|
|
||||||
for (let j = 0; j < childKeybinds.length; j++) {
|
|
||||||
addKeybind(childKeybinds[j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
categories["Workspace"].sort((a, b) => {
|
|
||||||
const dispA = a.dispatcher || ""
|
|
||||||
const dispB = b.dispatcher || ""
|
|
||||||
return dispA.localeCompare(dispB)
|
|
||||||
})
|
|
||||||
|
|
||||||
categories["Window"].sort((a, b) => {
|
|
||||||
const dispA = a.dispatcher || ""
|
|
||||||
const dispB = b.dispatcher || ""
|
|
||||||
return dispA.localeCompare(dispB)
|
|
||||||
})
|
|
||||||
|
|
||||||
categories["Monitor"].sort((a, b) => {
|
|
||||||
const dispA = a.dispatcher || ""
|
|
||||||
const dispB = b.dispatcher || ""
|
|
||||||
return dispA.localeCompare(dispB)
|
|
||||||
})
|
|
||||||
|
|
||||||
categories["Execute"].sort((a, b) => {
|
|
||||||
const modsA = a.mods || []
|
|
||||||
const keyA = a.key || ""
|
|
||||||
const bindA = [...modsA, keyA].join("+")
|
|
||||||
|
|
||||||
const modsB = b.mods || []
|
|
||||||
const keyB = b.key || ""
|
|
||||||
const bindB = [...modsB, keyB].join("+")
|
|
||||||
|
|
||||||
return bindA.localeCompare(bindB)
|
|
||||||
})
|
|
||||||
|
|
||||||
return categories
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
id: mainFlickable
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
contentWidth: rowLayout.implicitWidth
|
|
||||||
contentHeight: rowLayout.implicitHeight
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: rowLayout
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
property var categories: root.categorizeKeybinds()
|
|
||||||
property real columnWidth: (mainFlickable.width - spacing * 2) / 3
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: rowLayout.columnWidth
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Window / Monitor"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.primary
|
|
||||||
opacity: 0.3
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: 1; height: Theme.spacingXS }
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: [...(rowLayout.categories["Window"] || []), ...(rowLayout.categories["Monitor"] || [])]
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: Math.min(140, parent.width * 0.42)
|
|
||||||
height: 22
|
|
||||||
radius: 4
|
|
||||||
opacity: 0.3
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
width: parent.width - 4
|
|
||||||
text: {
|
|
||||||
const mods = modelData.mods || []
|
|
||||||
const key = modelData.key || ""
|
|
||||||
const parts = [...mods, key]
|
|
||||||
return parts.join("+")
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
isMonospace: true
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width - 150
|
|
||||||
text: {
|
|
||||||
const comment = modelData.comment || ""
|
|
||||||
if (comment) return comment
|
|
||||||
|
|
||||||
const dispatcher = modelData.dispatcher || ""
|
|
||||||
const params = modelData.params || ""
|
|
||||||
return params ? `${dispatcher} ${params}` : dispatcher
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
opacity: 0.9
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ["Workspace", "Execute"]
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: rowLayout.columnWidth
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.primary
|
|
||||||
opacity: 0.3
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: 1; height: Theme.spacingXS }
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: rowLayout.categories[modelData] || []
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: Math.min(140, parent.width * 0.42)
|
|
||||||
height: 22
|
|
||||||
radius: 4
|
|
||||||
opacity: 0.3
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
width: parent.width - 4
|
|
||||||
text: {
|
|
||||||
const mods = modelData.mods || []
|
|
||||||
const key = modelData.key || ""
|
|
||||||
const parts = [...mods, key]
|
|
||||||
return parts.join("+")
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
isMonospace: true
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width - 150
|
|
||||||
text: {
|
|
||||||
const comment = modelData.comment || ""
|
|
||||||
if (comment) return comment
|
|
||||||
|
|
||||||
const dispatcher = modelData.dispatcher || ""
|
|
||||||
const params = modelData.params || ""
|
|
||||||
return params ? `${dispatcher} ${params}` : dispatcher
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
opacity: 0.9
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,454 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int optionCount: SessionService.hibernateSupported ? 5 : 4
|
|
||||||
property rect parentBounds: Qt.rect(0, 0, 0, 0)
|
|
||||||
property var parentScreen: null
|
|
||||||
|
|
||||||
signal powerActionRequested(string action, string title, string message)
|
|
||||||
|
|
||||||
function openCentered() {
|
|
||||||
parentBounds = Qt.rect(0, 0, 0, 0)
|
|
||||||
parentScreen = null
|
|
||||||
backgroundOpacity = 0.5
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFromControlCenter(bounds, targetScreen) {
|
|
||||||
parentBounds = bounds
|
|
||||||
parentScreen = targetScreen
|
|
||||||
backgroundOpacity = 0
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOption(action) {
|
|
||||||
close();
|
|
||||||
const actions = {
|
|
||||||
"logout": {
|
|
||||||
"title": I18n.tr("Log Out"),
|
|
||||||
"message": I18n.tr("Are you sure you want to log out?")
|
|
||||||
},
|
|
||||||
"suspend": {
|
|
||||||
"title": I18n.tr("Suspend"),
|
|
||||||
"message": I18n.tr("Are you sure you want to suspend the system?")
|
|
||||||
},
|
|
||||||
"hibernate": {
|
|
||||||
"title": I18n.tr("Hibernate"),
|
|
||||||
"message": I18n.tr("Are you sure you want to hibernate the system?")
|
|
||||||
},
|
|
||||||
"reboot": {
|
|
||||||
"title": I18n.tr("Reboot"),
|
|
||||||
"message": I18n.tr("Are you sure you want to reboot the system?")
|
|
||||||
},
|
|
||||||
"poweroff": {
|
|
||||||
"title": I18n.tr("Power Off"),
|
|
||||||
"message": I18n.tr("Are you sure you want to power off the system?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const selected = actions[action]
|
|
||||||
if (selected) {
|
|
||||||
root.powerActionRequested(action, selected.title, selected.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
width: 320
|
|
||||||
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
|
|
||||||
enableShadow: true
|
|
||||||
screen: parentScreen
|
|
||||||
positioning: parentBounds.width > 0 ? "custom" : "center"
|
|
||||||
customPosition: {
|
|
||||||
if (parentBounds.width > 0) {
|
|
||||||
const centerX = parentBounds.x + (parentBounds.width - width) / 2
|
|
||||||
const centerY = parentBounds.y + (parentBounds.height - height) / 2
|
|
||||||
return Qt.point(centerX, centerY)
|
|
||||||
}
|
|
||||||
return Qt.point(0, 0)
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return close();
|
|
||||||
}
|
|
||||||
onOpened: () => {
|
|
||||||
selectedIndex = 0;
|
|
||||||
Qt.callLater(() => modalFocusScope.forceActiveFocus());
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: (event) => {
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Up:
|
|
||||||
case Qt.Key_Backtab:
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Down:
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
const actions = ["logout", "suspend"];
|
|
||||||
if (SessionService.hibernateSupported) actions.push("hibernate");
|
|
||||||
actions.push("reboot", "poweroff");
|
|
||||||
if (selectedIndex < actions.length) {
|
|
||||||
selectOption(actions[selectedIndex]);
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_N:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_P:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_J:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_K:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Options")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
return close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 0) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (logoutArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === 0 ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Log Out")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = 0;
|
|
||||||
selectOption("logout");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 1) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (suspendArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 1 ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === 1 ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Suspend")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = 1;
|
|
||||||
selectOption("suspend");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 2) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (hibernateArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 2 ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === 2 ? 1 : 0
|
|
||||||
visible: SessionService.hibernateSupported
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "ac_unit"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Hibernate")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: hibernateArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = 2;
|
|
||||||
selectOption("hibernate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
const rebootIndex = SessionService.hibernateSupported ? 3 : 2;
|
|
||||||
if (selectedIndex === rebootIndex) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (rebootArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "restart_alt"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Reboot")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = SessionService.hibernateSupported ? 3 : 2;
|
|
||||||
selectOption("reboot");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
const powerOffIndex = SessionService.hibernateSupported ? 4 : 3;
|
|
||||||
if (selectedIndex === powerOffIndex) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (powerOffArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power_settings_new"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Off")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: powerOffArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = SessionService.hibernateSupported ? 4 : 3;
|
|
||||||
selectOption("poweroff");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
height: Theme.spacingS
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: processListModal
|
|
||||||
|
|
||||||
property int currentTab: 0
|
|
||||||
property var tabNames: ["Processes", "Performance", "System"]
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
if (!DgopService.dgopAvailable) {
|
|
||||||
console.warn("ProcessListModal: dgop is not available");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
open();
|
|
||||||
UserInfoService.getUptime();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close();
|
|
||||||
if (processContextMenu.visible) {
|
|
||||||
processContextMenu.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (!DgopService.dgopAvailable) {
|
|
||||||
console.warn("ProcessListModal: dgop is not available");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
hide();
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 900
|
|
||||||
height: 680
|
|
||||||
visible: false
|
|
||||||
backgroundColor: Theme.popupBackground()
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
enableShadow: true
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: processesTabComponent
|
|
||||||
|
|
||||||
ProcessesTab {
|
|
||||||
contextMenu: processContextMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: performanceTabComponent
|
|
||||||
|
|
||||||
PerformanceTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: systemTabComponent
|
|
||||||
|
|
||||||
SystemTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessContextMenu {
|
|
||||||
id: processContextMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: (event) => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
processListModal.hide();
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_1) {
|
|
||||||
currentTab = 0;
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_2) {
|
|
||||||
currentTab = 1;
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_3) {
|
|
||||||
currentTab = 2;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error message when dgop is not available
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 400
|
|
||||||
height: 200
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
|
|
||||||
border.color: Theme.error
|
|
||||||
border.width: 2
|
|
||||||
visible: !DgopService.dgopAvailable
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "error"
|
|
||||||
size: 48
|
|
||||||
color: Theme.error
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("System Monitor Unavailable")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.error
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
visible: DgopService.dgopAvailable
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("System Monitor")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge + 4
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
return processListModal.hide();
|
|
||||||
}
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 52
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 4
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: tabNames
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
|
||||||
height: 44
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
|
||||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
|
||||||
border.width: currentTab === index ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
|
||||||
return tabIcons[index] || "tab";
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
|
||||||
opacity: currentTab === index ? 1 : 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.verticalCenterOffset: -1
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: tabMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
currentTab = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: processesTab
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: processListModal.visible && currentTab === 0
|
|
||||||
visible: currentTab === 0
|
|
||||||
opacity: currentTab === 0 ? 1 : 0
|
|
||||||
sourceComponent: processesTabComponent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: performanceTab
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: processListModal.visible && currentTab === 1
|
|
||||||
visible: currentTab === 1
|
|
||||||
opacity: currentTab === 1 ? 1 : 0
|
|
||||||
sourceComponent: performanceTabComponent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: systemTab
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: processListModal.visible && currentTab === 2
|
|
||||||
visible: currentTab === 2
|
|
||||||
opacity: currentTab === 2 ? 1 : 0
|
|
||||||
sourceComponent: systemTabComponent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Settings
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int currentIndex: 0
|
|
||||||
property var parentModal: null
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: 0
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: Theme.spacingM
|
|
||||||
anchors.topMargin: 0
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: personalizationLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 0
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
PersonalizationTab {
|
|
||||||
parentModal: root.parentModal
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: timeWeatherLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 1
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: TimeWeatherTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: topBarLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 2
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: DankBarTab {
|
|
||||||
parentModal: root.parentModal
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: widgetsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 3
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: WidgetTweaksTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: dockLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 4
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
DockTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: displaysLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 5
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: DisplaysTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: launcherLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 6
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: LauncherTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: themeColorsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 7
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: ThemeColorsTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: powerLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 8
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: PowerSettings {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: pluginsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 9
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: PluginsTab {
|
|
||||||
parentModal: root.parentModal
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: aboutLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.currentIndex === 10
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: AboutTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Modals.FileBrowser
|
|
||||||
import qs.Modules.Settings
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: settingsModal
|
|
||||||
|
|
||||||
property Component settingsContent
|
|
||||||
property alias profileBrowser: profileBrowser
|
|
||||||
property int currentTabIndex: 0
|
|
||||||
|
|
||||||
signal closingModal()
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
open();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
hide();
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
objectName: "settingsModal"
|
|
||||||
width: 800
|
|
||||||
height: 800
|
|
||||||
visible: false
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return hide();
|
|
||||||
}
|
|
||||||
content: settingsContent
|
|
||||||
onOpened: () => {
|
|
||||||
Qt.callLater(() => modalFocusScope.forceActiveFocus())
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: event => {
|
|
||||||
const tabCount = 11
|
|
||||||
if (event.key === Qt.Key_Down) {
|
|
||||||
currentTabIndex = (currentTabIndex + 1) % tabCount
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Tab && !event.modifiers) {
|
|
||||||
currentTabIndex = (currentTabIndex + 1) % tabCount
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
|
|
||||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
settingsModal.show();
|
|
||||||
return "SETTINGS_OPEN_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
settingsModal.hide();
|
|
||||||
return "SETTINGS_CLOSE_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
settingsModal.toggle();
|
|
||||||
return "SETTINGS_TOGGLE_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "settings"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function browse(type: string) {
|
|
||||||
if (type === "wallpaper") {
|
|
||||||
wallpaperBrowser.allowStacking = false;
|
|
||||||
wallpaperBrowser.open();
|
|
||||||
} else if (type === "profile") {
|
|
||||||
profileBrowser.allowStacking = false;
|
|
||||||
profileBrowser.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "file"
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: profileBrowser
|
|
||||||
|
|
||||||
allowStacking: true
|
|
||||||
parentModal: settingsModal
|
|
||||||
browserTitle: "Select Profile Image"
|
|
||||||
browserIcon: "person"
|
|
||||||
browserType: "profile"
|
|
||||||
showHiddenFiles: true
|
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
onFileSelected: (path) => {
|
|
||||||
PortalService.setProfileImage(path);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
onDialogClosed: () => {
|
|
||||||
allowStacking = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: wallpaperBrowser
|
|
||||||
|
|
||||||
allowStacking: true
|
|
||||||
parentModal: settingsModal
|
|
||||||
browserTitle: "Select Wallpaper"
|
|
||||||
browserIcon: "wallpaper"
|
|
||||||
browserType: "wallpaper"
|
|
||||||
showHiddenFiles: true
|
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
onFileSelected: (path) => {
|
|
||||||
SessionData.setWallpaper(path);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
onDialogClosed: () => {
|
|
||||||
allowStacking = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsContent: Component {
|
|
||||||
Item {
|
|
||||||
id: rootScope
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
anchors.bottomMargin: Theme.spacingL
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 35
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Settings")
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
return settingsModal.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 35
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
SettingsSidebar {
|
|
||||||
id: sidebar
|
|
||||||
|
|
||||||
parentModal: settingsModal
|
|
||||||
currentIndex: settingsModal.currentTabIndex
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
settingsModal.currentTabIndex = currentIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsContent {
|
|
||||||
id: content
|
|
||||||
|
|
||||||
width: parent.width - sidebar.width
|
|
||||||
height: parent.height
|
|
||||||
parentModal: settingsModal
|
|
||||||
currentIndex: settingsModal.currentTabIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Settings
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: sidebarContainer
|
|
||||||
|
|
||||||
property int currentIndex: 0
|
|
||||||
property var parentModal: null
|
|
||||||
readonly property var sidebarItems: [{
|
|
||||||
"text": I18n.tr("Personalization"),
|
|
||||||
"icon": "person"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Time & Weather"),
|
|
||||||
"icon": "schedule"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Dank Bar"),
|
|
||||||
"icon": "toolbar"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Widgets"),
|
|
||||||
"icon": "widgets"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Dock"),
|
|
||||||
"icon": "dock_to_bottom"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Displays"),
|
|
||||||
"icon": "monitor"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Launcher"),
|
|
||||||
"icon": "apps"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Theme & Colors"),
|
|
||||||
"icon": "palette"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Power & Security"),
|
|
||||||
"icon": "power"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("Plugins"),
|
|
||||||
"icon": "extension"
|
|
||||||
}, {
|
|
||||||
"text": I18n.tr("About"),
|
|
||||||
"icon": "info"
|
|
||||||
}]
|
|
||||||
|
|
||||||
function navigateNext() {
|
|
||||||
currentIndex = (currentIndex + 1) % sidebarItems.length
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigatePrevious() {
|
|
||||||
currentIndex = (currentIndex - 1 + sidebarItems.length) % sidebarItems.length
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 270
|
|
||||||
height: parent.height
|
|
||||||
color: Theme.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
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property int index
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
property bool isActive: sidebarContainer.currentIndex === index
|
|
||||||
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 44
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: modelData.icon || ""
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.text || ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
|
|
||||||
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: tabMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
sidebarContainer.currentIndex = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,244 +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
|
|
||||||
|
|
||||||
function resetScroll() {
|
|
||||||
resultsView.resetScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
clip: false
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
if (parentModal)
|
|
||||||
parentModal.hide()
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
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_J && event.modifiers & Qt.ControlModifier) {
|
|
||||||
appLauncher.selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
|
||||||
appLauncher.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Tab) {
|
|
||||||
if (appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow()
|
|
||||||
} else {
|
|
||||||
appLauncher.selectNext()
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Backtab) {
|
|
||||||
if (appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow()
|
|
||||||
} else {
|
|
||||||
appLauncher.selectPrevious()
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow()
|
|
||||||
} else {
|
|
||||||
appLauncher.selectNext()
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow()
|
|
||||||
} else {
|
|
||||||
appLauncher.selectPrevious()
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
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: appLauncher.viewMode !== "list"
|
|
||||||
ignoreTabKeys: 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_Tab || event.key === Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: 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 {
|
|
||||||
id: resultsView
|
|
||||||
appLauncher: spotlightKeyHandler.appLauncher
|
|
||||||
contextMenu: contextMenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotlightContextMenu {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
appLauncher: spotlightKeyHandler.appLauncher
|
|
||||||
parentHandler: spotlightKeyHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: contextMenu.visible
|
|
||||||
z: 999
|
|
||||||
onClicked: () => {
|
|
||||||
contextMenu.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
|
|
||||||
// Prevent closing when clicking on the menu itself
|
|
||||||
x: contextMenu.x
|
|
||||||
y: contextMenu.y
|
|
||||||
width: contextMenu.width
|
|
||||||
height: contextMenu.height
|
|
||||||
onClicked: () => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
property var currentApp: null
|
|
||||||
property var appLauncher: null
|
|
||||||
property var parentHandler: null
|
|
||||||
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
|
|
||||||
|
|
||||||
function show(x, y, app) {
|
|
||||||
currentApp = app
|
|
||||||
contextMenu.x = x + 4
|
|
||||||
contextMenu.y = y + 4
|
|
||||||
contextMenu.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
|
||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
padding: 0
|
|
||||||
closePolicy: Popup.CloseOnPressOutside
|
|
||||||
modal: false
|
|
||||||
dim: false
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enter: Transition {
|
|
||||||
NumberAnimation {
|
|
||||||
property: "opacity"
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit: Transition {
|
|
||||||
NumberAnimation {
|
|
||||||
property: "opacity"
|
|
||||||
from: 1
|
|
||||||
to: 0
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: pinRow
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (!desktopEntry)
|
|
||||||
return "push_pin"
|
|
||||||
|
|
||||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
|
||||||
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!desktopEntry)
|
|
||||||
return I18n.tr("Pin to Dock")
|
|
||||||
|
|
||||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
|
||||||
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: pinMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
if (!desktopEntry)
|
|
||||||
return
|
|
||||||
|
|
||||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
|
||||||
if (SessionData.isPinnedApp(appId))
|
|
||||||
SessionData.removePinnedApp(appId)
|
|
||||||
else
|
|
||||||
SessionData.addPinnedApp(appId)
|
|
||||||
contextMenu.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: desktopEntry && desktopEntry.actions ? desktopEntry.actions : []
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: actionRow
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Theme.iconSize - 2
|
|
||||||
height: Theme.iconSize - 2
|
|
||||||
visible: modelData.icon && modelData.icon !== ""
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: actionMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData && desktopEntry) {
|
|
||||||
SessionService.launchDesktopAction(desktopEntry, modelData)
|
|
||||||
if (appLauncher && contextMenu.currentApp) {
|
|
||||||
appLauncher.appLaunched(contextMenu.currentApp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contextMenu.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: desktopEntry && desktopEntry.actions && desktopEntry.actions.length > 0
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: launchRow
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "launch"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Launch")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: launchMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
if (contextMenu.currentApp && appLauncher)
|
|
||||||
appLauncher.launchApp(contextMenu.currentApp)
|
|
||||||
|
|
||||||
contextMenu.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: SessionService.hasPrimeRun
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: SessionService.hasPrimeRun
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: primeRunRow
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "memory"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Launch on dGPU")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: primeRunMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
if (desktopEntry) {
|
|
||||||
SessionService.launchDesktopEntry(desktopEntry, true)
|
|
||||||
if (appLauncher && contextMenu.currentApp) {
|
|
||||||
appLauncher.appLaunched(contextMenu.currentApp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contextMenu.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +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 alias spotlightContent: spotlightContentInstance
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
spotlightOpen = true
|
|
||||||
open()
|
|
||||||
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (spotlightContent && spotlightContent.searchField) {
|
|
||||||
spotlightContent.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWithQuery(query) {
|
|
||||||
if (spotlightContent) {
|
|
||||||
if (spotlightContent.appLauncher) {
|
|
||||||
spotlightContent.appLauncher.searchQuery = query
|
|
||||||
}
|
|
||||||
if (spotlightContent.searchField) {
|
|
||||||
spotlightContent.searchField.text = query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spotlightOpen = true
|
|
||||||
open()
|
|
||||||
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (spotlightContent && spotlightContent.searchField) {
|
|
||||||
spotlightContent.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
spotlightOpen = false
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
onDialogClosed: {
|
|
||||||
if (spotlightContent) {
|
|
||||||
if (spotlightContent.appLauncher) {
|
|
||||||
spotlightContent.appLauncher.searchQuery = ""
|
|
||||||
spotlightContent.appLauncher.selectedIndex = 0
|
|
||||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
|
|
||||||
}
|
|
||||||
if (spotlightContent.resetScroll) {
|
|
||||||
spotlightContent.resetScroll()
|
|
||||||
}
|
|
||||||
if (spotlightContent.searchField) {
|
|
||||||
spotlightContent.searchField.text = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (spotlightOpen) {
|
|
||||||
hide()
|
|
||||||
} else {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: spotlightOpen
|
|
||||||
width: 550
|
|
||||||
height: 700
|
|
||||||
backgroundColor: Theme.popupBackground()
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
borderColor: Theme.outlineMedium
|
|
||||||
borderWidth: 1
|
|
||||||
enableShadow: true
|
|
||||||
keepContentLoaded: true
|
|
||||||
onVisibleChanged: () => {
|
|
||||||
if (visible && !spotlightOpen) {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
if (visible && spotlightContent) {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (spotlightContent.searchField) {
|
|
||||||
spotlightContent.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
|
|
||||||
spotlightOpen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: ModalManager
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
spotlightModal.show()
|
|
||||||
return "SPOTLIGHT_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
spotlightModal.hide()
|
|
||||||
return "SPOTLIGHT_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
spotlightModal.toggle()
|
|
||||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function openQuery(query: string): string {
|
|
||||||
spotlightModal.showWithQuery(query)
|
|
||||||
return "SPOTLIGHT_OPEN_QUERY_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleQuery(query: string): string {
|
|
||||||
if (spotlightModal.spotlightOpen) {
|
|
||||||
spotlightModal.hide()
|
|
||||||
} else {
|
|
||||||
spotlightModal.showWithQuery(query)
|
|
||||||
}
|
|
||||||
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "spotlight"
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotlightContent {
|
|
||||||
id: spotlightContentInstance
|
|
||||||
|
|
||||||
parentModal: spotlightModal
|
|
||||||
}
|
|
||||||
|
|
||||||
directContent: spotlightContentInstance
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: resultsContainer
|
|
||||||
|
|
||||||
// DEVELOPER NOTE: This component renders the Spotlight launcher (accessed via Mod+Space).
|
|
||||||
// Changes to launcher behavior, especially item rendering, filtering, or model structure,
|
|
||||||
// likely require corresponding updates in Modules/AppDrawer/AppLauncher.qml and vice versa.
|
|
||||||
|
|
||||||
property var appLauncher: null
|
|
||||||
property var contextMenu: null
|
|
||||||
|
|
||||||
function resetScroll() {
|
|
||||||
resultsList.contentY = 0
|
|
||||||
resultsGrid.contentY = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - y
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: resultsList
|
|
||||||
|
|
||||||
property int itemHeight: 60
|
|
||||||
property int iconSize: 40
|
|
||||||
property bool showDescription: true
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
const itemY = index * (itemHeight + itemSpacing)
|
|
||||||
const itemBottom = itemY + itemHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
visible: appLauncher && appLauncher.viewMode === "list"
|
|
||||||
model: appLauncher ? appLauncher.model : null
|
|
||||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
|
||||||
clip: true
|
|
||||||
spacing: itemSpacing
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
onItemClicked: (index, modelData) => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
|
||||||
if (contextMenu)
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: () => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: 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
|
|
||||||
visible: model.icon !== undefined && model.icon !== ""
|
|
||||||
|
|
||||||
property string iconValue: model.icon || ""
|
|
||||||
property bool isMaterial: iconValue.indexOf("material:") === 0
|
|
||||||
property string materialName: isMaterial ? iconValue.substring(9) : ""
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: parent.materialName
|
|
||||||
size: resultsList.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
visible: parent.isMaterial
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: listIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: parent.isMaterial ? "" : Quickshell.iconPath(parent.iconValue, true)
|
|
||||||
asynchronous: true
|
|
||||||
visible: !parent.isMaterial && status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !parent.isMaterial && !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: (model.icon !== undefined && model.icon !== "") ? (parent.width - resultsList.iconSize - Theme.spacingL) : parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
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 && !model.isPlugin) {
|
|
||||||
const globalPos = mapToItem(null, mouse.x, mouse.y)
|
|
||||||
const modalPos = resultsContainer.parent.mapFromItem(null, globalPos.x, globalPos.y)
|
|
||||||
resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
visible: model.icon !== undefined && model.icon !== ""
|
|
||||||
|
|
||||||
property string iconValue: model.icon || ""
|
|
||||||
property bool isMaterial: iconValue.indexOf("material:") === 0
|
|
||||||
property string materialName: isMaterial ? iconValue.substring(9) : ""
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: parent.materialName
|
|
||||||
size: parent.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
visible: parent.isMaterial
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: gridIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: parent.isMaterial ? "" : Quickshell.iconPath(parent.iconValue, true)
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: !parent.isMaterial && status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !parent.isMaterial && !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: 1
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 && !model.isPlugin) {
|
|
||||||
const globalPos = mapToItem(null, mouse.x, mouse.y)
|
|
||||||
const modalPos = resultsContainer.parent.mapFromItem(null, globalPos.x, globalPos.y)
|
|
||||||
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,561 +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: ""
|
|
||||||
property string wifiUsernameInput: ""
|
|
||||||
property bool requiresEnterprise: false
|
|
||||||
|
|
||||||
property string wifiAnonymousIdentityInput: ""
|
|
||||||
property string wifiDomainInput: ""
|
|
||||||
|
|
||||||
property bool isPromptMode: false
|
|
||||||
property string promptToken: ""
|
|
||||||
property string promptReason: ""
|
|
||||||
property var promptFields: []
|
|
||||||
property string promptSetting: ""
|
|
||||||
|
|
||||||
function show(ssid) {
|
|
||||||
wifiPasswordSSID = ssid
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
isPromptMode = false
|
|
||||||
promptToken = ""
|
|
||||||
promptReason = ""
|
|
||||||
promptFields = []
|
|
||||||
promptSetting = ""
|
|
||||||
|
|
||||||
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
|
|
||||||
requiresEnterprise = network?.enterprise || false
|
|
||||||
|
|
||||||
open()
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item) {
|
|
||||||
if (requiresEnterprise && contentLoader.item.usernameInput) {
|
|
||||||
contentLoader.item.usernameInput.forceActiveFocus()
|
|
||||||
} else if (contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function showFromPrompt(token, ssid, setting, fields, hints, reason) {
|
|
||||||
wifiPasswordSSID = ssid
|
|
||||||
isPromptMode = true
|
|
||||||
promptToken = token
|
|
||||||
promptReason = reason
|
|
||||||
promptFields = fields || []
|
|
||||||
promptSetting = setting || "802-11-wireless-security"
|
|
||||||
|
|
||||||
requiresEnterprise = setting === "802-1x"
|
|
||||||
|
|
||||||
if (reason === "wrong-password") {
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
} else {
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
open()
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item) {
|
|
||||||
if (reason === "wrong-password" && contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.text = ""
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
} else if (requiresEnterprise && contentLoader.item.usernameInput) {
|
|
||||||
contentLoader.item.usernameInput.forceActiveFocus()
|
|
||||||
} else if (contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
width: 420
|
|
||||||
height: requiresEnterprise ? 430 : 230
|
|
||||||
onShouldBeVisibleChanged: () => {
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onOpened: {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item) {
|
|
||||||
if (requiresEnterprise && contentLoader.item.usernameInput) {
|
|
||||||
contentLoader.item.usernameInput.forceActiveFocus()
|
|
||||||
} else if (contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
NetworkService.cancelCredentials(promptToken)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: NetworkService
|
|
||||||
|
|
||||||
function onPasswordDialogShouldReopenChanged() {
|
|
||||||
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
|
|
||||||
wifiPasswordSSID = NetworkService.connectingSSID
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
open()
|
|
||||||
NetworkService.passwordDialogShouldReopen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
FocusScope {
|
|
||||||
id: wifiContent
|
|
||||||
|
|
||||||
property alias usernameInput: usernameInput
|
|
||||||
property alias passwordInput: passwordInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
NetworkService.cancelCredentials(promptToken)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 40
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Connect to Wi-Fi")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ")
|
|
||||||
return prefix + wifiPasswordSSID
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: isPromptMode && promptReason === "wrong-password"
|
|
||||||
text: I18n.tr("Incorrect password")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.error
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
NetworkService.cancelCredentials(promptToken)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: usernameInput.activeFocus ? 2 : 1
|
|
||||||
visible: requiresEnterprise
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: () => {
|
|
||||||
usernameInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: usernameInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiUsernameInput
|
|
||||||
placeholderText: I18n.tr("Username")
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiUsernameInput = text
|
|
||||||
}
|
|
||||||
onAccepted: () => {
|
|
||||||
if (passwordInput) {
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: passwordInput.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: () => {
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: passwordInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiPasswordInput
|
|
||||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
|
||||||
placeholderText: requiresEnterprise ? I18n.tr("Password") : ""
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
focus: !requiresEnterprise
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiPasswordInput = text
|
|
||||||
}
|
|
||||||
onAccepted: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
const secrets = {}
|
|
||||||
if (promptSetting === "802-11-wireless-security") {
|
|
||||||
secrets["psk"] = passwordInput.text
|
|
||||||
} else if (promptSetting === "802-1x") {
|
|
||||||
if (usernameInput.text) secrets["identity"] = usernameInput.text
|
|
||||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
|
||||||
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
|
|
||||||
}
|
|
||||||
NetworkService.submitCredentials(promptToken, secrets, true)
|
|
||||||
} else {
|
|
||||||
const username = requiresEnterprise ? usernameInput.text : ""
|
|
||||||
NetworkService.connectToWifi(
|
|
||||||
wifiPasswordSSID,
|
|
||||||
passwordInput.text,
|
|
||||||
username,
|
|
||||||
wifiAnonymousIdentityInput,
|
|
||||||
wifiDomainInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
passwordInput.text = ""
|
|
||||||
if (requiresEnterprise) usernameInput.text = ""
|
|
||||||
}
|
|
||||||
Component.onCompleted: () => {
|
|
||||||
if (root.shouldBeVisible && !requiresEnterprise)
|
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: focusDelayTimer
|
|
||||||
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
onTriggered: () => {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
if (requiresEnterprise && usernameInput) {
|
|
||||||
usernameInput.forceActiveFocus()
|
|
||||||
} else {
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: requiresEnterprise
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: anonInput.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: () => {
|
|
||||||
anonInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: anonInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiAnonymousIdentityInput
|
|
||||||
placeholderText: I18n.tr("Anonymous Identity (optional)")
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiAnonymousIdentityInput = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: requiresEnterprise
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: domainMatchInput.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: () => {
|
|
||||||
domainMatchInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: domainMatchInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiDomainInput
|
|
||||||
placeholderText: I18n.tr("Domain (optional)")
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiDomainInput = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: showPasswordCheckbox
|
|
||||||
|
|
||||||
property bool checked: false
|
|
||||||
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
radius: 4
|
|
||||||
color: checked ? Theme.primary : "transparent"
|
|
||||||
border.color: checked ? Theme.primary : Theme.outlineButton
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "check"
|
|
||||||
size: 12
|
|
||||||
color: Theme.background
|
|
||||||
visible: parent.checked
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Show password")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: cancelText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: I18n.tr("Cancel")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cancelArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
NetworkService.cancelCredentials(promptToken)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
|
||||||
enabled: requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0
|
|
||||||
opacity: enabled ? 1 : 0.5
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: connectText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: I18n.tr("Connect")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.background
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: connectArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: parent.enabled
|
|
||||||
onClicked: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
const secrets = {}
|
|
||||||
if (promptSetting === "802-11-wireless-security") {
|
|
||||||
secrets["psk"] = passwordInput.text
|
|
||||||
} else if (promptSetting === "802-1x") {
|
|
||||||
if (usernameInput.text) secrets["identity"] = usernameInput.text
|
|
||||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
|
||||||
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
|
|
||||||
}
|
|
||||||
NetworkService.submitCredentials(promptToken, secrets, true)
|
|
||||||
} else {
|
|
||||||
const username = requiresEnterprise ? usernameInput.text : ""
|
|
||||||
NetworkService.connectToWifi(
|
|
||||||
wifiPasswordSSID,
|
|
||||||
passwordInput.text,
|
|
||||||
username,
|
|
||||||
wifiAnonymousIdentityInput,
|
|
||||||
wifiDomainInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
passwordInput.text = ""
|
|
||||||
if (requiresEnterprise) usernameInput.text = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: VpnService
|
|
||||||
}
|
|
||||||
|
|
||||||
ccWidgetIcon: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off")
|
|
||||||
ccWidgetPrimaryText: "VPN"
|
|
||||||
ccWidgetSecondaryText: {
|
|
||||||
if (!VpnService.connected)
|
|
||||||
return "Disconnected"
|
|
||||||
const names = VpnService.activeNames || []
|
|
||||||
if (names.length <= 1)
|
|
||||||
return names[0] || "Connected"
|
|
||||||
return names[0] + " +" + (names.length - 1)
|
|
||||||
}
|
|
||||||
ccWidgetIsActive: VpnService.connected
|
|
||||||
|
|
||||||
onCcWidgetToggled: {
|
|
||||||
if (VpnService.connected) {
|
|
||||||
VpnService.disconnectAllActive()
|
|
||||||
} else if (VpnService.profiles.length > 0) {
|
|
||||||
VpnService.connect(VpnService.profiles[0].uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ccDetailContent: Component {
|
|
||||||
Rectangle {
|
|
||||||
id: detailRoot
|
|
||||||
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: detailColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!VpnService.connected)
|
|
||||||
return "Active: None"
|
|
||||||
const names = VpnService.activeNames || []
|
|
||||||
if (names.length <= 1)
|
|
||||||
return "Active: " + (names[0] || "VPN")
|
|
||||||
return "Active: " + names[0] + " +" + (names.length - 1)
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
|
|
||||||
visible: VpnService.connected
|
|
||||||
width: 110
|
|
||||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "link_off"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Disconnect")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: discAllArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: VpnService.disconnectAllActive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: 1
|
|
||||||
width: parent.width
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
width: parent.width
|
|
||||||
height: 160
|
|
||||||
contentHeight: listCol.height
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: listCol
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: VpnService.profiles.length === 0 ? 120 : 0
|
|
||||||
visible: height > 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "playlist_remove"
|
|
||||||
size: 36
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("No VPN profiles found")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Add a VPN in NetworkManager")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: VpnService.profiles
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
width: parent ? parent.width : 300
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: rowArea.containsMouse ? Theme.primaryHoverLight : (VpnService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
|
|
||||||
border.width: VpnService.isActiveUuid(modelData.uuid) ? 2 : 1
|
|
||||||
border.color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: VpnService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (modelData.type === "wireguard")
|
|
||||||
return "WireGuard"
|
|
||||||
const svc = modelData.serviceType || ""
|
|
||||||
if (svc.indexOf("openvpn") !== -1)
|
|
||||||
return "OpenVPN"
|
|
||||||
if (svc.indexOf("wireguard") !== -1)
|
|
||||||
return "WireGuard (plugin)"
|
|
||||||
if (svc.indexOf("openconnect") !== -1)
|
|
||||||
return "OpenConnect"
|
|
||||||
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1)
|
|
||||||
return "Fortinet"
|
|
||||||
if (svc.indexOf("strongswan") !== -1)
|
|
||||||
return "IPsec (strongSwan)"
|
|
||||||
if (svc.indexOf("libreswan") !== -1)
|
|
||||||
return "IPsec (Libreswan)"
|
|
||||||
if (svc.indexOf("l2tp") !== -1)
|
|
||||||
return "L2TP/IPsec"
|
|
||||||
if (svc.indexOf("pptp") !== -1)
|
|
||||||
return "PPTP"
|
|
||||||
if (svc.indexOf("vpnc") !== -1)
|
|
||||||
return "Cisco (vpnc)"
|
|
||||||
if (svc.indexOf("sstp") !== -1)
|
|
||||||
return "SSTP"
|
|
||||||
if (svc)
|
|
||||||
return svc.split('.').pop()
|
|
||||||
return "VPN"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rowArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: VpnService.toggle(modelData.uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool hasVolumeSliderInCC: {
|
|
||||||
const widgets = SettingsData.controlCenterWidgets || []
|
|
||||||
return widgets.some(widget => widget.id === "volumeSlider")
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: headerRow
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: headerText
|
|
||||||
text: I18n.tr("Audio Devices")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: volumeSlider
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingXS
|
|
||||||
height: 35
|
|
||||||
spacing: 0
|
|
||||||
visible: !hasVolumeSliderInCC
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
height: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
|
||||||
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: iconArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
if (!AudioService.sink || !AudioService.sink.audio) return "volume_off"
|
|
||||||
let muted = AudioService.sink.audio.muted
|
|
||||||
let volume = AudioService.sink.audio.volume
|
|
||||||
if (muted || volume === 0.0) return "volume_off"
|
|
||||||
if (volume <= 0.33) return "volume_down"
|
|
||||||
if (volume <= 0.66) return "volume_up"
|
|
||||||
return "volume_up"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
|
||||||
enabled: AudioService.sink && AudioService.sink.audio
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0
|
|
||||||
showValue: true
|
|
||||||
unit: "%"
|
|
||||||
valueOverride: actualVolumePercent
|
|
||||||
thumbOutlineColor: Theme.surfaceVariant
|
|
||||||
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.volume = newValue / 100
|
|
||||||
if (newValue > 0 && AudioService.sink.audio.muted) {
|
|
||||||
AudioService.sink.audio.muted = false
|
|
||||||
}
|
|
||||||
AudioService.volumeChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
id: audioContent
|
|
||||||
anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
|
|
||||||
contentHeight: audioColumn.height
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: audioColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Pipewire.nodes.values.filter(node => {
|
|
||||||
return node.audio && node.isSink && !node.isStream
|
|
||||||
})
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (modelData.name.includes("bluez"))
|
|
||||||
return "headset"
|
|
||||||
else if (modelData.name.includes("hdmi"))
|
|
||||||
return "tv"
|
|
||||||
else if (modelData.name.includes("usb"))
|
|
||||||
return "headset"
|
|
||||||
else
|
|
||||||
return "speaker"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: AudioService.displayName(modelData)
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData === AudioService.sink ? "Active" : "Available"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: deviceMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData) {
|
|
||||||
Pipewire.preferredDefaultAudioSink = modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string currentDeviceName: ""
|
|
||||||
property string instanceId: ""
|
|
||||||
|
|
||||||
signal deviceNameChanged(string newDeviceName)
|
|
||||||
|
|
||||||
implicitHeight: brightnessContent.height + Theme.spacingM
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
id: brightnessContent
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
contentHeight: brightnessColumn.height
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: brightnessColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 100
|
|
||||||
visible: !DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
name: DisplayService.brightnessAvailable ? "brightness_6" : "error"
|
|
||||||
size: 32
|
|
||||||
color: DisplayService.brightnessAvailable ? Theme.primary : Theme.error
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: DisplayService.brightnessAvailable ? "No brightness devices available" : "Brightness control not available"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: DisplayService.devices || []
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHighest
|
|
||||||
border.color: modelData.name === currentDeviceName ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
border.width: modelData.name === currentDeviceName ? 2 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
const deviceClass = modelData.class || ""
|
|
||||||
const deviceName = modelData.name || ""
|
|
||||||
|
|
||||||
if (deviceClass === "backlight" || deviceClass === "ddc") {
|
|
||||||
const brightness = modelData.percentage || 50
|
|
||||||
if (brightness <= 33) return "brightness_low"
|
|
||||||
if (brightness <= 66) return "brightness_medium"
|
|
||||||
return "brightness_high"
|
|
||||||
} else if (deviceName.includes("kbd")) {
|
|
||||||
return "keyboard"
|
|
||||||
} else {
|
|
||||||
return "lightbulb"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: modelData.name === currentDeviceName ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (modelData.percentage || 50) + "%"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - 50 - Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
const name = modelData.name || ""
|
|
||||||
const deviceClass = modelData.class || ""
|
|
||||||
if (deviceClass === "backlight") {
|
|
||||||
return name.replace("_", " ").replace(/\b\w/g, c => c.toUpperCase())
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
const deviceClass = modelData.class || ""
|
|
||||||
if (deviceClass === "backlight") return "Backlight device"
|
|
||||||
if (deviceClass === "ddc") return "DDC/CI monitor"
|
|
||||||
if (deviceClass === "leds") return "LED device"
|
|
||||||
return deviceClass
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
currentDeviceName = modelData.name
|
|
||||||
deviceNameChanged(modelData.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property string powerOptionsText: I18n.tr("Power Options")
|
|
||||||
readonly property string logOutText: I18n.tr("Log Out")
|
|
||||||
readonly property string suspendText: I18n.tr("Suspend")
|
|
||||||
readonly property string rebootText: I18n.tr("Reboot")
|
|
||||||
readonly property string powerOffText: I18n.tr("Power Off")
|
|
||||||
|
|
||||||
property bool powerMenuVisible: false
|
|
||||||
signal powerActionRequested(string action, string title, string message)
|
|
||||||
|
|
||||||
visible: powerMenuVisible
|
|
||||||
implicitWidth: 400
|
|
||||||
implicitHeight: 320
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.min(320, parent.width - Theme.spacingL * 2)
|
|
||||||
height: 320 // Fixed height to prevent cropping
|
|
||||||
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
|
||||||
y: Theme.barHeight + Theme.spacingXS
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 0
|
|
||||||
opacity: powerMenuVisible ? 1 : 0
|
|
||||||
scale: powerMenuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.powerOptionsText
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.logOutText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.powerActionRequested(
|
|
||||||
"logout", "Log Out",
|
|
||||||
"Are you sure you want to log out?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.suspendText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.powerActionRequested(
|
|
||||||
"suspend", "Suspend",
|
|
||||||
"Are you sure you want to suspend the system?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
|
|
||||||
Theme.warning.g,
|
|
||||||
Theme.warning.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "restart_alt"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.rebootText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.powerActionRequested(
|
|
||||||
"reboot", "Reboot",
|
|
||||||
"Are you sure you want to reboot the system?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power_settings_new"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.powerOffText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: powerOffArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.powerActionRequested(
|
|
||||||
"poweroff", "Power Off",
|
|
||||||
"Are you sure you want to power off the system?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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,136 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string deviceName: ""
|
|
||||||
property string instanceId: ""
|
|
||||||
|
|
||||||
signal iconClicked()
|
|
||||||
|
|
||||||
height: 40
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
property string targetDeviceName: {
|
|
||||||
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceName && deviceName.length > 0) {
|
|
||||||
const found = DisplayService.devices.find(dev => dev.name === deviceName)
|
|
||||||
return found ? found.name : ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentDeviceName = DisplayService.currentDevice
|
|
||||||
if (currentDeviceName) {
|
|
||||||
const found = DisplayService.devices.find(dev => dev.name === currentDeviceName)
|
|
||||||
return found ? found.name : ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : ""
|
|
||||||
}
|
|
||||||
|
|
||||||
property var targetDevice: {
|
|
||||||
if (!targetDeviceName || !DisplayService.devices) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return DisplayService.devices.find(dev => dev.name === targetDeviceName) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
property real targetBrightness: {
|
|
||||||
if (!targetDeviceName) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return DisplayService.getDeviceBrightness(targetDeviceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
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)
|
|
||||||
: Theme.withAlpha(Theme.primary, 0)
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: iconArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: DisplayService.devices && DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (DisplayService.devices && DisplayService.devices.length > 1) {
|
|
||||||
root.iconClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onEntered: {
|
|
||||||
tooltipLoader.active = true
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
const tooltipText = targetDevice ? "bl device: " + targetDevice.name : "Backlight Control"
|
|
||||||
const p = iconArea.mapToItem(null, iconArea.width / 2, 0)
|
|
||||||
tooltipLoader.item.show(tooltipText, p.x, p.y - 40, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: {
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hide()
|
|
||||||
}
|
|
||||||
tooltipLoader.active = false
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
if (!DisplayService.brightnessAvailable || !targetDevice) {
|
|
||||||
return "brightness_low"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetDevice.class === "backlight" || targetDevice.class === "ddc") {
|
|
||||||
const brightness = targetBrightness
|
|
||||||
if (brightness <= 33) return "brightness_low"
|
|
||||||
if (brightness <= 66) return "brightness_medium"
|
|
||||||
return "brightness_high"
|
|
||||||
} else if (targetDevice.name.includes("kbd")) {
|
|
||||||
return "keyboard"
|
|
||||||
} else {
|
|
||||||
return "lightbulb"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: DisplayService.brightnessAvailable && targetDevice && targetBrightness > 0 ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
|
||||||
enabled: DisplayService.brightnessAvailable && targetDeviceName.length > 0
|
|
||||||
minimum: 1
|
|
||||||
maximum: 100
|
|
||||||
value: targetBrightness
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (DisplayService.brightnessAvailable && targetDeviceName) {
|
|
||||||
DisplayService.setBrightness(newValue, targetDeviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
|
||||||
trackColor: Theme.surfaceContainerHigh
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: tooltipLoader
|
|
||||||
active: false
|
|
||||||
sourceComponent: DankTooltip {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property var barWindow
|
|
||||||
required property var axis
|
|
||||||
required property var appDrawerLoader
|
|
||||||
required property var dankDashPopoutLoader
|
|
||||||
required property var processListPopoutLoader
|
|
||||||
required property var notificationCenterLoader
|
|
||||||
required property var batteryPopoutLoader
|
|
||||||
required property var vpnPopoutLoader
|
|
||||||
required property var controlCenterLoader
|
|
||||||
required property var clipboardHistoryModalPopup
|
|
||||||
required property var systemUpdateLoader
|
|
||||||
required property var notepadInstance
|
|
||||||
|
|
||||||
property alias reveal: core.reveal
|
|
||||||
property alias autoHide: core.autoHide
|
|
||||||
property alias backgroundTransparency: core.backgroundTransparency
|
|
||||||
property alias hasActivePopout: core.hasActivePopout
|
|
||||||
property alias mouseArea: topBarMouseArea
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: inputMask
|
|
||||||
|
|
||||||
readonly property int barThickness: barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing)
|
|
||||||
|
|
||||||
readonly property bool showing: SettingsData.dankBarVisible && (core.reveal
|
|
||||||
|| (CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview)
|
|
||||||
|| !core.autoHide)
|
|
||||||
|
|
||||||
readonly property int maskThickness: showing ? barThickness : 1
|
|
||||||
|
|
||||||
x: {
|
|
||||||
if (!axis.isVertical) {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
switch (SettingsData.dankBarPosition) {
|
|
||||||
case SettingsData.Position.Left: return 0
|
|
||||||
case SettingsData.Position.Right: return parent.width - maskThickness
|
|
||||||
default: return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (axis.isVertical) {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
switch (SettingsData.dankBarPosition) {
|
|
||||||
case SettingsData.Position.Top: return 0
|
|
||||||
case SettingsData.Position.Bottom: return parent.height - maskThickness
|
|
||||||
default: return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width: axis.isVertical ? maskThickness : parent.width
|
|
||||||
height: axis.isVertical ? parent.height : maskThickness
|
|
||||||
}
|
|
||||||
|
|
||||||
Region {
|
|
||||||
id: mask
|
|
||||||
item: inputMask
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias maskRegion: mask
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: core
|
|
||||||
|
|
||||||
property real backgroundTransparency: SettingsData.dankBarTransparency
|
|
||||||
property bool autoHide: SettingsData.dankBarAutoHide
|
|
||||||
property bool revealSticky: false
|
|
||||||
|
|
||||||
property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false
|
|
||||||
|
|
||||||
readonly property bool hasActivePopout: {
|
|
||||||
const loaders = [{
|
|
||||||
"loader": appDrawerLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": dankDashPopoutLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": processListPopoutLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": notificationCenterLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": batteryPopoutLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": vpnPopoutLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": controlCenterLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": clipboardHistoryModalPopup,
|
|
||||||
"prop": "visible"
|
|
||||||
}, {
|
|
||||||
"loader": systemUpdateLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}]
|
|
||||||
return notepadInstanceVisible || loaders.some(item => {
|
|
||||||
if (item.loader) {
|
|
||||||
return item.loader?.item?.[item.prop]
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool reveal: {
|
|
||||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
|
||||||
return SettingsData.dankBarOpenOnOverview
|
|
||||||
}
|
|
||||||
return SettingsData.dankBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky)
|
|
||||||
}
|
|
||||||
|
|
||||||
onHasActivePopoutChanged: {
|
|
||||||
if (!hasActivePopout && autoHide && !topBarMouseArea.containsMouse) {
|
|
||||||
revealSticky = true
|
|
||||||
revealHold.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: revealHold
|
|
||||||
interval: 250
|
|
||||||
repeat: false
|
|
||||||
onTriggered: core.revealSticky = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onDankBarTransparencyChanged() {
|
|
||||||
core.backgroundTransparency = SettingsData.dankBarTransparency
|
|
||||||
}
|
|
||||||
|
|
||||||
target: SettingsData
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: topBarMouseArea
|
|
||||||
function onContainsMouseChanged() {
|
|
||||||
if (topBarMouseArea.containsMouse) {
|
|
||||||
core.revealSticky = true
|
|
||||||
revealHold.stop()
|
|
||||||
} else {
|
|
||||||
if (core.autoHide && !core.hasActivePopout) {
|
|
||||||
revealHold.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: topBarMouseArea
|
|
||||||
y: !barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? parent.height - height : 0) : 0
|
|
||||||
x: barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.width - width : 0) : 0
|
|
||||||
height: !barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
|
|
||||||
width: barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
|
|
||||||
anchors {
|
|
||||||
left: !barWindow.isVertical ? parent.left : (SettingsData.dankBarPosition === SettingsData.Position.Left ? parent.left : undefined)
|
|
||||||
right: !barWindow.isVertical ? parent.right : (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.right : undefined)
|
|
||||||
top: barWindow.isVertical ? parent.top : undefined
|
|
||||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
|
||||||
}
|
|
||||||
hoverEnabled: SettingsData.dankBarAutoHide && !core.reveal
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
enabled: SettingsData.dankBarAutoHide && !core.reveal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property var barWindow
|
|
||||||
required property var axis
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.leftMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
|
|
||||||
anchors.rightMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
|
|
||||||
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
|
|
||||||
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
|
|
||||||
|
|
||||||
readonly property real dpr: {
|
|
||||||
if (CompositorService.isNiri && barWindow.screen) {
|
|
||||||
const niriScale = NiriService.displayScales[barWindow.screen.name]
|
|
||||||
if (niriScale !== undefined) return niriScale
|
|
||||||
}
|
|
||||||
if (CompositorService.isHyprland && barWindow.screen) {
|
|
||||||
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === barWindow.screen.name)
|
|
||||||
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
|
|
||||||
}
|
|
||||||
return barWindow.screen?.devicePixelRatio || 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestRepaint() {
|
|
||||||
debounceTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: debounceTimer
|
|
||||||
interval: 50
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
barShape.requestPaint()
|
|
||||||
barTint.requestPaint()
|
|
||||||
barBorder.requestPaint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Canvas {
|
|
||||||
id: barShape
|
|
||||||
anchors.fill: parent
|
|
||||||
antialiasing: true
|
|
||||||
renderTarget: Canvas.FramebufferObject
|
|
||||||
renderStrategy: Canvas.Cooperative
|
|
||||||
|
|
||||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
|
||||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
|
||||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
|
||||||
|
|
||||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
|
||||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
|
||||||
|
|
||||||
onWingChanged: root.requestRepaint()
|
|
||||||
onRtChanged: root.requestRepaint()
|
|
||||||
onCorrectWidthChanged: root.requestRepaint()
|
|
||||||
onCorrectHeightChanged: root.requestRepaint()
|
|
||||||
onVisibleChanged: if (visible) root.requestRepaint()
|
|
||||||
Component.onCompleted: root.requestRepaint()
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onDprChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: barWindow
|
|
||||||
function on_BgColorChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: Theme
|
|
||||||
function onIsLightModeChanged() { root.requestRepaint() }
|
|
||||||
function onSurfaceContainerChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaint: {
|
|
||||||
const ctx = getContext("2d")
|
|
||||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
|
||||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
|
||||||
const R = wing
|
|
||||||
const RT = rt
|
|
||||||
const H = H_raw - (R > 0 ? R : 0)
|
|
||||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
|
||||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
|
||||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
|
||||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
|
||||||
|
|
||||||
function drawTopPath() {
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(RT, 0)
|
|
||||||
ctx.lineTo(W - RT, 0)
|
|
||||||
ctx.arcTo(W, 0, W, RT, RT)
|
|
||||||
ctx.lineTo(W, H)
|
|
||||||
|
|
||||||
if (R > 0) {
|
|
||||||
ctx.lineTo(W, H + R)
|
|
||||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
|
||||||
ctx.lineTo(R, H)
|
|
||||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
|
||||||
ctx.lineTo(0, H + R)
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(W, H - RT)
|
|
||||||
ctx.arcTo(W, H, W - RT, H, RT)
|
|
||||||
ctx.lineTo(RT, H)
|
|
||||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.lineTo(0, RT)
|
|
||||||
ctx.arcTo(0, 0, RT, 0, RT)
|
|
||||||
ctx.closePath()
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.reset()
|
|
||||||
ctx.clearRect(0, 0, W, H_raw)
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
if (isBottom) {
|
|
||||||
ctx.translate(W, H_raw)
|
|
||||||
ctx.rotate(Math.PI)
|
|
||||||
} else if (isLeft) {
|
|
||||||
ctx.translate(0, W)
|
|
||||||
ctx.rotate(-Math.PI / 2)
|
|
||||||
} else if (isRight) {
|
|
||||||
ctx.translate(H_raw, 0)
|
|
||||||
ctx.rotate(Math.PI / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
drawTopPath()
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
ctx.fillStyle = barWindow._bgColor
|
|
||||||
ctx.fill()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Canvas {
|
|
||||||
id: barTint
|
|
||||||
anchors.fill: parent
|
|
||||||
antialiasing: true
|
|
||||||
renderTarget: Canvas.FramebufferObject
|
|
||||||
renderStrategy: Canvas.Cooperative
|
|
||||||
|
|
||||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
|
||||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
|
||||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
|
||||||
|
|
||||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
|
||||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
|
||||||
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
|
|
||||||
|
|
||||||
onWingChanged: root.requestRepaint()
|
|
||||||
onRtChanged: root.requestRepaint()
|
|
||||||
onAlphaTintChanged: root.requestRepaint()
|
|
||||||
onCorrectWidthChanged: root.requestRepaint()
|
|
||||||
onCorrectHeightChanged: root.requestRepaint()
|
|
||||||
onVisibleChanged: if (visible) root.requestRepaint()
|
|
||||||
Component.onCompleted: root.requestRepaint()
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onDprChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: barWindow
|
|
||||||
function on_BgColorChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: Theme
|
|
||||||
function onIsLightModeChanged() { root.requestRepaint() }
|
|
||||||
function onSurfaceChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaint: {
|
|
||||||
const ctx = getContext("2d")
|
|
||||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
|
||||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
|
||||||
const R = wing
|
|
||||||
const RT = rt
|
|
||||||
const H = H_raw - (R > 0 ? R : 0)
|
|
||||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
|
||||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
|
||||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
|
||||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
|
||||||
|
|
||||||
function drawTopPath() {
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(RT, 0)
|
|
||||||
ctx.lineTo(W - RT, 0)
|
|
||||||
ctx.arcTo(W, 0, W, RT, RT)
|
|
||||||
ctx.lineTo(W, H)
|
|
||||||
|
|
||||||
if (R > 0) {
|
|
||||||
ctx.lineTo(W, H + R)
|
|
||||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
|
||||||
ctx.lineTo(R, H)
|
|
||||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
|
||||||
ctx.lineTo(0, H + R)
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(W, H - RT)
|
|
||||||
ctx.arcTo(W, H, W - RT, H, RT)
|
|
||||||
ctx.lineTo(RT, H)
|
|
||||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.lineTo(0, RT)
|
|
||||||
ctx.arcTo(0, 0, RT, 0, RT)
|
|
||||||
ctx.closePath()
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.reset()
|
|
||||||
ctx.clearRect(0, 0, W, H_raw)
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
if (isBottom) {
|
|
||||||
ctx.translate(W, H_raw)
|
|
||||||
ctx.rotate(Math.PI)
|
|
||||||
} else if (isLeft) {
|
|
||||||
ctx.translate(0, W)
|
|
||||||
ctx.rotate(-Math.PI / 2)
|
|
||||||
} else if (isRight) {
|
|
||||||
ctx.translate(H_raw, 0)
|
|
||||||
ctx.rotate(Math.PI / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
drawTopPath()
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
ctx.fillStyle = Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, alphaTint)
|
|
||||||
ctx.fill()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Canvas {
|
|
||||||
id: barBorder
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: SettingsData.dankBarBorderEnabled
|
|
||||||
renderTarget: Canvas.FramebufferObject
|
|
||||||
renderStrategy: Canvas.Cooperative
|
|
||||||
|
|
||||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
|
||||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
|
||||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
|
||||||
|
|
||||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
|
||||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
|
||||||
property bool borderEnabled: SettingsData.dankBarBorderEnabled
|
|
||||||
|
|
||||||
antialiasing: rt > 0 || wing > 0
|
|
||||||
|
|
||||||
onWingChanged: root.requestRepaint()
|
|
||||||
onRtChanged: root.requestRepaint()
|
|
||||||
onBorderEnabledChanged: root.requestRepaint()
|
|
||||||
onCorrectWidthChanged: root.requestRepaint()
|
|
||||||
onCorrectHeightChanged: root.requestRepaint()
|
|
||||||
onVisibleChanged: if (visible) root.requestRepaint()
|
|
||||||
Component.onCompleted: root.requestRepaint()
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onDprChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: Theme
|
|
||||||
function onIsLightModeChanged() { root.requestRepaint() }
|
|
||||||
function onSurfaceTextChanged() { root.requestRepaint() }
|
|
||||||
function onPrimaryChanged() { root.requestRepaint() }
|
|
||||||
function onSecondaryChanged() { root.requestRepaint() }
|
|
||||||
function onOutlineChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onDankBarBorderColorChanged() { root.requestRepaint() }
|
|
||||||
function onDankBarBorderOpacityChanged() { root.requestRepaint() }
|
|
||||||
function onDankBarBorderThicknessChanged() { root.requestRepaint() }
|
|
||||||
function onDankBarSpacingChanged() { root.requestRepaint() }
|
|
||||||
function onDankBarSquareCornersChanged() { root.requestRepaint() }
|
|
||||||
function onDankBarTransparencyChanged() { root.requestRepaint() }
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaint: {
|
|
||||||
if (!borderEnabled) return
|
|
||||||
|
|
||||||
const ctx = getContext("2d")
|
|
||||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
|
||||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
|
||||||
const R = wing
|
|
||||||
const RT = rt
|
|
||||||
const H = H_raw - (R > 0 ? R : 0)
|
|
||||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
|
||||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
|
||||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
|
||||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
|
||||||
|
|
||||||
const spacing = SettingsData.dankBarSpacing
|
|
||||||
const hasEdgeGap = spacing > 0 || RT > 0
|
|
||||||
|
|
||||||
ctx.reset()
|
|
||||||
ctx.clearRect(0, 0, W, H_raw)
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
if (isBottom) {
|
|
||||||
ctx.translate(W, H_raw)
|
|
||||||
ctx.rotate(Math.PI)
|
|
||||||
} else if (isLeft) {
|
|
||||||
ctx.translate(0, W)
|
|
||||||
ctx.rotate(-Math.PI / 2)
|
|
||||||
} else if (isRight) {
|
|
||||||
ctx.translate(H_raw, 0)
|
|
||||||
ctx.rotate(Math.PI / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const uiThickness = Math.max(1, SettingsData.dankBarBorderThickness ?? 1)
|
|
||||||
const devThickness = Math.max(1, Math.round(Theme.px(uiThickness, dpr)))
|
|
||||||
|
|
||||||
const key = SettingsData.dankBarBorderColor || "surfaceText"
|
|
||||||
const base = (key === "surfaceText") ? Theme.surfaceText
|
|
||||||
: (key === "primary") ? Theme.primary
|
|
||||||
: Theme.secondary
|
|
||||||
const color = Theme.withAlpha(base, SettingsData.dankBarBorderOpacity ?? 1.0)
|
|
||||||
|
|
||||||
ctx.globalCompositeOperation = "source-over"
|
|
||||||
ctx.fillStyle = color
|
|
||||||
|
|
||||||
function drawTopBorder() {
|
|
||||||
if (!hasEdgeGap) {
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.rect(0, H - devThickness, W, devThickness)
|
|
||||||
ctx.fill()
|
|
||||||
} else {
|
|
||||||
const thk = devThickness
|
|
||||||
const RTi = Math.max(0, RT - thk)
|
|
||||||
const Ri = Math.max(0, R - thk)
|
|
||||||
|
|
||||||
ctx.beginPath()
|
|
||||||
|
|
||||||
if (R > 0 && Ri > 0) {
|
|
||||||
ctx.moveTo(RT, 0)
|
|
||||||
ctx.lineTo(W - RT, 0)
|
|
||||||
ctx.arcTo(W, 0, W, RT, RT)
|
|
||||||
ctx.lineTo(W, H)
|
|
||||||
ctx.lineTo(W, H + R)
|
|
||||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
|
||||||
ctx.lineTo(R, H)
|
|
||||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
|
||||||
ctx.lineTo(0, H + R)
|
|
||||||
ctx.lineTo(0, RT)
|
|
||||||
ctx.arcTo(0, 0, RT, 0, RT)
|
|
||||||
ctx.closePath()
|
|
||||||
|
|
||||||
ctx.moveTo(RT, thk)
|
|
||||||
ctx.arcTo(thk, thk, thk, RT, RTi)
|
|
||||||
ctx.lineTo(thk, H + R)
|
|
||||||
ctx.arc(R, H + R, Ri, -Math.PI, -Math.PI / 2, false)
|
|
||||||
ctx.lineTo(W - R, H + thk)
|
|
||||||
ctx.arc(W - R, H + R, Ri, -Math.PI / 2, 0, false)
|
|
||||||
ctx.lineTo(W - thk, H + R)
|
|
||||||
ctx.lineTo(W - thk, RT)
|
|
||||||
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
|
|
||||||
ctx.lineTo(RT, thk)
|
|
||||||
ctx.closePath()
|
|
||||||
} else {
|
|
||||||
ctx.moveTo(RT, 0)
|
|
||||||
ctx.lineTo(W - RT, 0)
|
|
||||||
ctx.arcTo(W, 0, W, RT, RT)
|
|
||||||
ctx.lineTo(W, H - RT)
|
|
||||||
ctx.arcTo(W, H, W - RT, H, RT)
|
|
||||||
ctx.lineTo(RT, H)
|
|
||||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
|
||||||
ctx.lineTo(0, RT)
|
|
||||||
ctx.arcTo(0, 0, RT, 0, RT)
|
|
||||||
ctx.closePath()
|
|
||||||
|
|
||||||
ctx.moveTo(RT, thk)
|
|
||||||
ctx.arcTo(thk, thk, thk, RT, RTi)
|
|
||||||
ctx.lineTo(thk, H - RT)
|
|
||||||
ctx.arcTo(thk, H - thk, RT, H - thk, RTi)
|
|
||||||
ctx.lineTo(W - RT, H - thk)
|
|
||||||
ctx.arcTo(W - thk, H - thk, W - thk, H - RT, RTi)
|
|
||||||
ctx.lineTo(W - thk, RT)
|
|
||||||
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
|
|
||||||
ctx.lineTo(RT, thk)
|
|
||||||
ctx.closePath()
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fill("evenodd")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawTopBorder()
|
|
||||||
ctx.restore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,460 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var widgetsModel: null
|
|
||||||
property var components: null
|
|
||||||
property bool noBackground: false
|
|
||||||
required property var axis
|
|
||||||
property string section: "center"
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
|
|
||||||
readonly property bool isVertical: axis?.isVertical ?? false
|
|
||||||
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
|
|
||||||
|
|
||||||
property var centerWidgets: []
|
|
||||||
property int totalWidgets: 0
|
|
||||||
property real totalSize: 0
|
|
||||||
|
|
||||||
function updateLayout() {
|
|
||||||
const containerSize = isVertical ? height : width
|
|
||||||
if (containerSize <= 0 || !visible) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
centerWidgets = []
|
|
||||||
totalWidgets = 0
|
|
||||||
totalSize = 0
|
|
||||||
|
|
||||||
let configuredWidgets = 0
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
const item = centerRepeater.itemAt(i)
|
|
||||||
if (item && getWidgetVisible(item.widgetId)) {
|
|
||||||
configuredWidgets++
|
|
||||||
if (item.active && item.item) {
|
|
||||||
centerWidgets.push(item.item)
|
|
||||||
totalWidgets++
|
|
||||||
totalSize += isVertical ? item.item.height : item.item.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWidgets > 1) {
|
|
||||||
totalSize += spacing * (totalWidgets - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
positionWidgets(configuredWidgets)
|
|
||||||
}
|
|
||||||
|
|
||||||
function positionWidgets(configuredWidgets) {
|
|
||||||
if (totalWidgets === 0 || (isVertical ? height : width) <= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentCenter = (isVertical ? height : width) / 2
|
|
||||||
const isOdd = configuredWidgets % 2 === 1
|
|
||||||
|
|
||||||
centerWidgets.forEach(widget => {
|
|
||||||
if (isVertical) {
|
|
||||||
widget.anchors.verticalCenter = undefined
|
|
||||||
} else {
|
|
||||||
widget.anchors.horizontalCenter = undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isOdd) {
|
|
||||||
const middleIndex = Math.floor(configuredWidgets / 2)
|
|
||||||
let currentActiveIndex = 0
|
|
||||||
let middleWidget = null
|
|
||||||
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
const item = centerRepeater.itemAt(i)
|
|
||||||
if (item && getWidgetVisible(item.widgetId)) {
|
|
||||||
if (currentActiveIndex === middleIndex && item.active && item.item) {
|
|
||||||
middleWidget = item.item
|
|
||||||
break
|
|
||||||
}
|
|
||||||
currentActiveIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (middleWidget) {
|
|
||||||
const middleSize = isVertical ? middleWidget.height : middleWidget.width
|
|
||||||
if (isVertical) {
|
|
||||||
middleWidget.y = parentCenter - (middleSize / 2)
|
|
||||||
} else {
|
|
||||||
middleWidget.x = parentCenter - (middleSize / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
let leftWidgets = []
|
|
||||||
let rightWidgets = []
|
|
||||||
let foundMiddle = false
|
|
||||||
|
|
||||||
for (var i = 0; i < centerWidgets.length; i++) {
|
|
||||||
if (centerWidgets[i] === middleWidget) {
|
|
||||||
foundMiddle = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!foundMiddle) {
|
|
||||||
leftWidgets.push(centerWidgets[i])
|
|
||||||
} else {
|
|
||||||
rightWidgets.push(centerWidgets[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPos = isVertical ? middleWidget.y : middleWidget.x
|
|
||||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
|
||||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
|
||||||
currentPos -= (spacing + size)
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
leftWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
|
|
||||||
for (var i = 0; i < rightWidgets.length; i++) {
|
|
||||||
currentPos += spacing
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
rightWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let configuredLeftIndex = (configuredWidgets / 2) - 1
|
|
||||||
let configuredRightIndex = configuredWidgets / 2
|
|
||||||
const halfSpacing = spacing / 2
|
|
||||||
|
|
||||||
let leftWidget = null
|
|
||||||
let rightWidget = null
|
|
||||||
let leftWidgets = []
|
|
||||||
let rightWidgets = []
|
|
||||||
|
|
||||||
let currentConfigIndex = 0
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
const item = centerRepeater.itemAt(i)
|
|
||||||
if (item && getWidgetVisible(item.widgetId)) {
|
|
||||||
if (item.active && item.item) {
|
|
||||||
if (currentConfigIndex < configuredLeftIndex) {
|
|
||||||
leftWidgets.push(item.item)
|
|
||||||
} else if (currentConfigIndex === configuredLeftIndex) {
|
|
||||||
leftWidget = item.item
|
|
||||||
} else if (currentConfigIndex === configuredRightIndex) {
|
|
||||||
rightWidget = item.item
|
|
||||||
} else {
|
|
||||||
rightWidgets.push(item.item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentConfigIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftWidget && rightWidget) {
|
|
||||||
const leftSize = isVertical ? leftWidget.height : leftWidget.width
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidget.y = parentCenter - halfSpacing - leftSize
|
|
||||||
rightWidget.y = parentCenter + halfSpacing
|
|
||||||
} else {
|
|
||||||
leftWidget.x = parentCenter - halfSpacing - leftSize
|
|
||||||
rightWidget.x = parentCenter + halfSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPos = isVertical ? leftWidget.y : leftWidget.x
|
|
||||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
|
||||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
|
||||||
currentPos -= (spacing + size)
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
leftWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
|
|
||||||
for (var i = 0; i < rightWidgets.length; i++) {
|
|
||||||
currentPos += spacing
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
rightWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
|
||||||
}
|
|
||||||
} else if (leftWidget && !rightWidget) {
|
|
||||||
const leftSize = isVertical ? leftWidget.height : leftWidget.width
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidget.y = parentCenter - halfSpacing - leftSize
|
|
||||||
} else {
|
|
||||||
leftWidget.x = parentCenter - halfSpacing - leftSize
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPos = isVertical ? leftWidget.y : leftWidget.x
|
|
||||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
|
||||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
|
||||||
currentPos -= (spacing + size)
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
leftWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPos = (isVertical ? leftWidget.y + leftWidget.height : leftWidget.x + leftWidget.width) + spacing
|
|
||||||
for (var i = 0; i < rightWidgets.length; i++) {
|
|
||||||
currentPos += spacing
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
rightWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
|
||||||
}
|
|
||||||
} else if (!leftWidget && rightWidget) {
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidget.y = parentCenter + halfSpacing
|
|
||||||
} else {
|
|
||||||
rightWidget.x = parentCenter + halfSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPos = (isVertical ? rightWidget.y : rightWidget.x) - spacing
|
|
||||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
|
||||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
|
||||||
currentPos -= size
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
leftWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos -= spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
|
|
||||||
for (var i = 0; i < rightWidgets.length; i++) {
|
|
||||||
currentPos += spacing
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
rightWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
|
||||||
}
|
|
||||||
} else if (totalWidgets === 1 && centerWidgets[0]) {
|
|
||||||
const size = isVertical ? centerWidgets[0].height : centerWidgets[0].width
|
|
||||||
if (isVertical) {
|
|
||||||
centerWidgets[0].y = parentCenter - (size / 2)
|
|
||||||
} else {
|
|
||||||
centerWidgets[0].x = parentCenter - (size / 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWidgetVisible(widgetId) {
|
|
||||||
const widgetVisibility = {
|
|
||||||
"cpuUsage": DgopService.dgopAvailable,
|
|
||||||
"memUsage": DgopService.dgopAvailable,
|
|
||||||
"cpuTemp": DgopService.dgopAvailable,
|
|
||||||
"gpuTemp": DgopService.dgopAvailable,
|
|
||||||
"network_speed_monitor": DgopService.dgopAvailable
|
|
||||||
}
|
|
||||||
return widgetVisibility[widgetId] ?? true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWidgetComponent(widgetId) {
|
|
||||||
// Build dynamic component map including plugins
|
|
||||||
let baseMap = {
|
|
||||||
"launcherButton": "launcherButtonComponent",
|
|
||||||
"workspaceSwitcher": "workspaceSwitcherComponent",
|
|
||||||
"focusedWindow": "focusedWindowComponent",
|
|
||||||
"runningApps": "runningAppsComponent",
|
|
||||||
"clock": "clockComponent",
|
|
||||||
"music": "mediaComponent",
|
|
||||||
"weather": "weatherComponent",
|
|
||||||
"systemTray": "systemTrayComponent",
|
|
||||||
"privacyIndicator": "privacyIndicatorComponent",
|
|
||||||
"clipboard": "clipboardComponent",
|
|
||||||
"cpuUsage": "cpuUsageComponent",
|
|
||||||
"memUsage": "memUsageComponent",
|
|
||||||
"diskUsage": "diskUsageComponent",
|
|
||||||
"cpuTemp": "cpuTempComponent",
|
|
||||||
"gpuTemp": "gpuTempComponent",
|
|
||||||
"notificationButton": "notificationButtonComponent",
|
|
||||||
"battery": "batteryComponent",
|
|
||||||
"controlCenterButton": "controlCenterButtonComponent",
|
|
||||||
"idleInhibitor": "idleInhibitorComponent",
|
|
||||||
"spacer": "spacerComponent",
|
|
||||||
"separator": "separatorComponent",
|
|
||||||
"network_speed_monitor": "networkComponent",
|
|
||||||
"keyboard_layout_name": "keyboardLayoutNameComponent",
|
|
||||||
"vpn": "vpnComponent",
|
|
||||||
"notepadButton": "notepadButtonComponent",
|
|
||||||
"colorPicker": "colorPickerComponent",
|
|
||||||
"systemUpdate": "systemUpdateComponent"
|
|
||||||
}
|
|
||||||
|
|
||||||
// For built-in components, get from components property
|
|
||||||
const componentKey = baseMap[widgetId]
|
|
||||||
if (componentKey && root.components[componentKey]) {
|
|
||||||
return root.components[componentKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
// For plugin components, get from PluginService
|
|
||||||
var parts = widgetId.split(":")
|
|
||||||
var pluginId = parts[0]
|
|
||||||
let pluginComponents = PluginService.getWidgetComponents()
|
|
||||||
return pluginComponents[pluginId] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
height: parent.height
|
|
||||||
width: parent.width
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: layoutTimer
|
|
||||||
interval: 0
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.updateLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
onWidthChanged: {
|
|
||||||
if (width > 0) {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHeightChanged: {
|
|
||||||
if (height > 0) {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && (isVertical ? height : width) > 0) {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: centerRepeater
|
|
||||||
model: root.widgetsModel
|
|
||||||
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
property string widgetId: model.widgetId
|
|
||||||
property var widgetData: model
|
|
||||||
property int spacerSize: model.size || 20
|
|
||||||
|
|
||||||
anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined
|
|
||||||
anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined
|
|
||||||
active: root.getWidgetVisible(model.widgetId) && (model.widgetId !== "music" || MprisController.activePlayer !== null)
|
|
||||||
sourceComponent: root.getWidgetComponent(model.widgetId)
|
|
||||||
opacity: (model.enabled !== false) ? 1 : 0
|
|
||||||
asynchronous: false
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (!item) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
item.widthChanged.connect(() => layoutTimer.restart())
|
|
||||||
item.heightChanged.connect(() => layoutTimer.restart())
|
|
||||||
if (model.widgetId === "spacer") {
|
|
||||||
item.spacerSize = Qt.binding(() => model.size || 20)
|
|
||||||
}
|
|
||||||
if (root.axis && "axis" in item) {
|
|
||||||
item.axis = Qt.binding(() => root.axis)
|
|
||||||
}
|
|
||||||
if (root.axis && "isVertical" in item) {
|
|
||||||
try {
|
|
||||||
item.isVertical = Qt.binding(() => root.axis.isVertical)
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject properties for plugin widgets
|
|
||||||
if ("section" in item) {
|
|
||||||
item.section = root.section
|
|
||||||
}
|
|
||||||
if ("parentScreen" in item) {
|
|
||||||
item.parentScreen = Qt.binding(() => root.parentScreen)
|
|
||||||
}
|
|
||||||
if ("widgetThickness" in item) {
|
|
||||||
item.widgetThickness = Qt.binding(() => root.widgetThickness)
|
|
||||||
}
|
|
||||||
if ("barThickness" in item) {
|
|
||||||
item.barThickness = Qt.binding(() => root.barThickness)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject PluginService for plugin widgets
|
|
||||||
if (item.pluginService !== undefined) {
|
|
||||||
var parts = model.widgetId.split(":")
|
|
||||||
var pluginId = parts[0]
|
|
||||||
var variantId = parts.length > 1 ? parts[1] : null
|
|
||||||
|
|
||||||
if (item.pluginId !== undefined) {
|
|
||||||
item.pluginId = pluginId
|
|
||||||
}
|
|
||||||
if (item.variantId !== undefined) {
|
|
||||||
item.variantId = variantId
|
|
||||||
}
|
|
||||||
if (item.variantData !== undefined && variantId) {
|
|
||||||
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
|
|
||||||
}
|
|
||||||
item.pluginService = PluginService
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.popoutService !== undefined) {
|
|
||||||
item.popoutService = PopoutService
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
onActiveChanged: {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: widgetsModel
|
|
||||||
function onCountChanged() {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for plugin changes and refresh components
|
|
||||||
Connections {
|
|
||||||
target: PluginService
|
|
||||||
function onPluginLoaded(pluginId) {
|
|
||||||
// Force refresh of component lookups
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
var item = centerRepeater.itemAt(i)
|
|
||||||
if (item && item.widgetId.startsWith(pluginId)) {
|
|
||||||
item.sourceComponent = root.getWidgetComponent(item.widgetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onPluginUnloaded(pluginId) {
|
|
||||||
// Force refresh of component lookups
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
var item = centerRepeater.itemAt(i)
|
|
||||||
if (item && item.widgetId.startsWith(pluginId)) {
|
|
||||||
item.sourceComponent = root.getWidgetComponent(item.widgetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,82 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var widgetsModel: null
|
|
||||||
property var components: null
|
|
||||||
property bool noBackground: false
|
|
||||||
required property var axis
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
|
|
||||||
readonly property bool isVertical: axis?.isVertical ?? false
|
|
||||||
|
|
||||||
implicitHeight: layoutLoader.item ? (layoutLoader.item.implicitHeight || layoutLoader.item.height) : 0
|
|
||||||
implicitWidth: layoutLoader.item ? (layoutLoader.item.implicitWidth || layoutLoader.item.width) : 0
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: layoutLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
sourceComponent: root.isVertical ? columnComp : rowComp
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: rowComp
|
|
||||||
Row {
|
|
||||||
spacing: noBackground ? 2 : Theme.spacingXS
|
|
||||||
Repeater {
|
|
||||||
model: root.widgetsModel
|
|
||||||
Item {
|
|
||||||
width: widgetLoader.item ? widgetLoader.item.width : 0
|
|
||||||
height: widgetLoader.item ? widgetLoader.item.height : 0
|
|
||||||
WidgetHost {
|
|
||||||
id: widgetLoader
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
widgetId: model.widgetId
|
|
||||||
widgetData: model
|
|
||||||
spacerSize: model.size || 20
|
|
||||||
components: root.components
|
|
||||||
isInColumn: false
|
|
||||||
axis: root.axis
|
|
||||||
section: "left"
|
|
||||||
parentScreen: root.parentScreen
|
|
||||||
widgetThickness: root.widgetThickness
|
|
||||||
barThickness: root.barThickness
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: columnComp
|
|
||||||
Column {
|
|
||||||
width: Math.max(parent.width, 200)
|
|
||||||
spacing: noBackground ? 2 : Theme.spacingXS
|
|
||||||
Repeater {
|
|
||||||
model: root.widgetsModel
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: widgetLoader.item ? widgetLoader.item.height : 0
|
|
||||||
WidgetHost {
|
|
||||||
id: widgetLoader
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
widgetId: model.widgetId
|
|
||||||
widgetData: model
|
|
||||||
spacerSize: model.size || 20
|
|
||||||
components: root.components
|
|
||||||
isInColumn: true
|
|
||||||
axis: root.axis
|
|
||||||
section: "left"
|
|
||||||
parentScreen: root.parentScreen
|
|
||||||
widgetThickness: root.widgetThickness
|
|
||||||
barThickness: root.barThickness
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
// No external details import; content inlined for consistency
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankPopout {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: VpnService
|
|
||||||
}
|
|
||||||
|
|
||||||
property var triggerScreen: null
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
triggerX = x;
|
|
||||||
triggerY = y;
|
|
||||||
triggerWidth = width;
|
|
||||||
triggerSection = section;
|
|
||||||
triggerScreen = screen;
|
|
||||||
}
|
|
||||||
|
|
||||||
popupWidth: 360
|
|
||||||
popupHeight: Math.min(Screen.height - 100, contentLoader.item ? contentLoader.item.implicitHeight : 260)
|
|
||||||
triggerX: Screen.width - 380 - Theme.spacingL
|
|
||||||
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
|
|
||||||
triggerWidth: 70
|
|
||||||
positioning: ""
|
|
||||||
screen: triggerScreen
|
|
||||||
shouldBeVisible: false
|
|
||||||
visible: shouldBeVisible
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Rectangle {
|
|
||||||
id: content
|
|
||||||
|
|
||||||
implicitHeight: contentColumn.height + Theme.spacingL * 2
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 0
|
|
||||||
antialiasing: true
|
|
||||||
smooth: true
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: function(event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
root.close();
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outer subtle shadow rings to match BatteryPopout
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -3
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 3
|
|
||||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
|
||||||
border.width: 0
|
|
||||||
z: -3
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -2
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 2
|
|
||||||
border.color: Theme.shadowMedium
|
|
||||||
border.width: 0
|
|
||||||
z: -2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 0
|
|
||||||
radius: parent.radius
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("VPN Connections")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close button (matches BatteryPopout)
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "close"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: closeArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: root.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inlined VPN details
|
|
||||||
Rectangle {
|
|
||||||
id: vpnDetail
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
implicitHeight: detailsColumn.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.getContentBackgroundAlpha() * 0.6)
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 0
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: detailsColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!VpnService.connected) {
|
|
||||||
return "Active: None";
|
|
||||||
}
|
|
||||||
|
|
||||||
const names = VpnService.activeNames || [];
|
|
||||||
if (names.length <= 1) {
|
|
||||||
return "Active: " + (names[0] || "VPN");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Active: " + names[0] + " +" + (names.length - 1);
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removed Quick Connect for clarity
|
|
||||||
Item {
|
|
||||||
width: 1
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect all (shown only when any active)
|
|
||||||
Rectangle {
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
|
|
||||||
visible: VpnService.connected
|
|
||||||
width: 130
|
|
||||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
|
||||||
border.width: 0
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "link_off"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Disconnect")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: discAllArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: VpnService.disconnectAllActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: 1
|
|
||||||
width: parent.width
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
width: parent.width
|
|
||||||
height: 160
|
|
||||||
contentHeight: listCol.height
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: listCol
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: VpnService.profiles.length === 0 ? 120 : 0
|
|
||||||
visible: height > 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "playlist_remove"
|
|
||||||
size: 36
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("No VPN profiles found")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Add a VPN in NetworkManager")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: VpnService.profiles
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
width: parent ? parent.width : 300
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: rowArea.containsMouse ? Theme.primaryHoverLight : (VpnService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
|
|
||||||
border.width: VpnService.isActiveUuid(modelData.uuid) ? 2 : 1
|
|
||||||
border.color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: VpnService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (modelData.type === "wireguard") {
|
|
||||||
return "WireGuard";
|
|
||||||
}
|
|
||||||
|
|
||||||
const svc = modelData.serviceType || "";
|
|
||||||
if (svc.indexOf("openvpn") !== -1) {
|
|
||||||
return "OpenVPN";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("wireguard") !== -1) {
|
|
||||||
return "WireGuard (plugin)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("openconnect") !== -1) {
|
|
||||||
return "OpenConnect";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1) {
|
|
||||||
return "Fortinet";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("strongswan") !== -1) {
|
|
||||||
return "IPsec (strongSwan)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("libreswan") !== -1) {
|
|
||||||
return "IPsec (Libreswan)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("l2tp") !== -1) {
|
|
||||||
return "L2TP/IPsec";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("pptp") !== -1) {
|
|
||||||
return "PPTP";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("vpnc") !== -1) {
|
|
||||||
return "Cisco (vpnc)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc.indexOf("sstp") !== -1) {
|
|
||||||
return "SSTP";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (svc) {
|
|
||||||
const parts = svc.split('.');
|
|
||||||
return parts[parts.length - 1];
|
|
||||||
}
|
|
||||||
return "VPN";
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rowArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: VpnService.toggle(modelData.uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
height: 1
|
|
||||||
width: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var widgetsModel: null
|
|
||||||
property var components: null
|
|
||||||
property bool noBackground: false
|
|
||||||
required property var axis
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
|
|
||||||
readonly property bool isVertical: axis?.isVertical ?? false
|
|
||||||
|
|
||||||
implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
|
|
||||||
implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: layoutLoader
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
sourceComponent: root.isVertical ? columnComp : rowComp
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: rowComp
|
|
||||||
Row {
|
|
||||||
spacing: noBackground ? 2 : Theme.spacingXS
|
|
||||||
anchors.right: parent ? parent.right : undefined
|
|
||||||
Repeater {
|
|
||||||
model: root.widgetsModel
|
|
||||||
Item {
|
|
||||||
width: widgetLoader.item ? widgetLoader.item.width : 0
|
|
||||||
height: widgetLoader.item ? widgetLoader.item.height : 0
|
|
||||||
WidgetHost {
|
|
||||||
id: widgetLoader
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
widgetId: model.widgetId
|
|
||||||
widgetData: model
|
|
||||||
spacerSize: model.size || 20
|
|
||||||
components: root.components
|
|
||||||
isInColumn: false
|
|
||||||
axis: root.axis
|
|
||||||
section: "right"
|
|
||||||
parentScreen: root.parentScreen
|
|
||||||
widgetThickness: root.widgetThickness
|
|
||||||
barThickness: root.barThickness
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: columnComp
|
|
||||||
Column {
|
|
||||||
width: parent ? parent.width : 0
|
|
||||||
spacing: noBackground ? 2 : Theme.spacingXS
|
|
||||||
Repeater {
|
|
||||||
model: root.widgetsModel
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: widgetLoader.item ? widgetLoader.item.height : 0
|
|
||||||
WidgetHost {
|
|
||||||
id: widgetLoader
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
widgetId: model.widgetId
|
|
||||||
widgetData: model
|
|
||||||
spacerSize: model.size || 20
|
|
||||||
components: root.components
|
|
||||||
isInColumn: true
|
|
||||||
axis: root.axis
|
|
||||||
section: "right"
|
|
||||||
parentScreen: root.parentScreen
|
|
||||||
widgetThickness: root.widgetThickness
|
|
||||||
barThickness: root.barThickness
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string widgetId: ""
|
|
||||||
property var widgetData: null
|
|
||||||
property int spacerSize: 20
|
|
||||||
property var components: null
|
|
||||||
property bool isInColumn: false
|
|
||||||
property var axis: null
|
|
||||||
property string section: "center"
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
|
|
||||||
asynchronous: false
|
|
||||||
|
|
||||||
active: getWidgetVisible(widgetId, DgopService.dgopAvailable) &&
|
|
||||||
(widgetId !== "music" || MprisController.activePlayer !== null)
|
|
||||||
sourceComponent: getWidgetComponent(widgetId, components)
|
|
||||||
opacity: getWidgetEnabled(widgetData?.enabled) ? 1 : 0
|
|
||||||
|
|
||||||
signal contentItemReady(var item)
|
|
||||||
|
|
||||||
Binding {
|
|
||||||
target: root.item
|
|
||||||
when: root.item && "parentScreen" in root.item
|
|
||||||
property: "parentScreen"
|
|
||||||
value: root.parentScreen
|
|
||||||
restoreMode: Binding.RestoreNone
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
|
||||||
target: root.item
|
|
||||||
when: root.item && "section" in root.item
|
|
||||||
property: "section"
|
|
||||||
value: root.section
|
|
||||||
restoreMode: Binding.RestoreNone
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
|
||||||
target: root.item
|
|
||||||
when: root.item && "widgetThickness" in root.item
|
|
||||||
property: "widgetThickness"
|
|
||||||
value: root.widgetThickness
|
|
||||||
restoreMode: Binding.RestoreNone
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
|
||||||
target: root.item
|
|
||||||
when: root.item && "barThickness" in root.item
|
|
||||||
property: "barThickness"
|
|
||||||
value: root.barThickness
|
|
||||||
restoreMode: Binding.RestoreNone
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
|
||||||
target: root.item
|
|
||||||
when: root.item && "axis" in root.item
|
|
||||||
property: "axis"
|
|
||||||
value: root.axis
|
|
||||||
restoreMode: Binding.RestoreNone
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
|
||||||
target: root.item
|
|
||||||
when: root.item && "widgetData" in root.item
|
|
||||||
property: "widgetData"
|
|
||||||
value: root.widgetData
|
|
||||||
restoreMode: Binding.RestoreNone
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (item) {
|
|
||||||
contentItemReady(item)
|
|
||||||
if (axis && "isVertical" in item) {
|
|
||||||
try {
|
|
||||||
item.isVertical = axis.isVertical
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.pluginService !== undefined) {
|
|
||||||
var parts = widgetId.split(":")
|
|
||||||
var pluginId = parts[0]
|
|
||||||
var variantId = parts.length > 1 ? parts[1] : null
|
|
||||||
|
|
||||||
if (item.pluginId !== undefined) {
|
|
||||||
item.pluginId = pluginId
|
|
||||||
}
|
|
||||||
if (item.variantId !== undefined) {
|
|
||||||
item.variantId = variantId
|
|
||||||
}
|
|
||||||
if (item.variantData !== undefined && variantId) {
|
|
||||||
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
|
|
||||||
}
|
|
||||||
item.pluginService = PluginService
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.popoutService !== undefined) {
|
|
||||||
item.popoutService = PopoutService
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWidgetComponent(widgetId, components) {
|
|
||||||
const componentMap = {
|
|
||||||
"launcherButton": components.launcherButtonComponent,
|
|
||||||
"workspaceSwitcher": components.workspaceSwitcherComponent,
|
|
||||||
"focusedWindow": components.focusedWindowComponent,
|
|
||||||
"runningApps": components.runningAppsComponent,
|
|
||||||
"clock": components.clockComponent,
|
|
||||||
"music": components.mediaComponent,
|
|
||||||
"weather": components.weatherComponent,
|
|
||||||
"systemTray": components.systemTrayComponent,
|
|
||||||
"privacyIndicator": components.privacyIndicatorComponent,
|
|
||||||
"clipboard": components.clipboardComponent,
|
|
||||||
"cpuUsage": components.cpuUsageComponent,
|
|
||||||
"memUsage": components.memUsageComponent,
|
|
||||||
"diskUsage": components.diskUsageComponent,
|
|
||||||
"cpuTemp": components.cpuTempComponent,
|
|
||||||
"gpuTemp": components.gpuTempComponent,
|
|
||||||
"notificationButton": components.notificationButtonComponent,
|
|
||||||
"battery": components.batteryComponent,
|
|
||||||
"controlCenterButton": components.controlCenterButtonComponent,
|
|
||||||
"idleInhibitor": components.idleInhibitorComponent,
|
|
||||||
"spacer": components.spacerComponent,
|
|
||||||
"separator": components.separatorComponent,
|
|
||||||
"network_speed_monitor": components.networkComponent,
|
|
||||||
"keyboard_layout_name": components.keyboardLayoutNameComponent,
|
|
||||||
"vpn": components.vpnComponent,
|
|
||||||
"notepadButton": components.notepadButtonComponent,
|
|
||||||
"colorPicker": components.colorPickerComponent,
|
|
||||||
"systemUpdate": components.systemUpdateComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentMap[widgetId]) {
|
|
||||||
return componentMap[widgetId]
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = widgetId.split(":")
|
|
||||||
var pluginId = parts[0]
|
|
||||||
|
|
||||||
let pluginMap = PluginService.getWidgetComponents()
|
|
||||||
return pluginMap[pluginId] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWidgetVisible(widgetId, dgopAvailable) {
|
|
||||||
const widgetVisibility = {
|
|
||||||
"cpuUsage": dgopAvailable,
|
|
||||||
"memUsage": dgopAvailable,
|
|
||||||
"cpuTemp": dgopAvailable,
|
|
||||||
"gpuTemp": dgopAvailable,
|
|
||||||
"network_speed_monitor": dgopAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
return widgetVisibility[widgetId] ?? true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWidgetEnabled(enabled) {
|
|
||||||
return enabled !== false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
|
||||||
readonly property bool hasActiveMedia: activePlayer !== null
|
|
||||||
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
|
||||||
|
|
||||||
width: 20
|
|
||||||
height: Theme.iconSize
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
active: isPlaying
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
Ref {
|
|
||||||
service: CavaService
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: fallbackTimer
|
|
||||||
|
|
||||||
running: !CavaService.cavaAvailable && isPlaying
|
|
||||||
interval: 256
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 1.5
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: 6
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 2
|
|
||||||
height: {
|
|
||||||
if (root.isPlaying && CavaService.values.length > index) {
|
|
||||||
const rawLevel = CavaService.values[index] || 0;
|
|
||||||
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
|
|
||||||
const maxHeight = Theme.iconSize - 2;
|
|
||||||
const minHeight = 3;
|
|
||||||
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
|
|
||||||
}
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
radius: 1.5
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.standardDecel
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
BasePill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property var popoutTarget: null
|
|
||||||
property var widgetData: null
|
|
||||||
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
|
|
||||||
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
|
|
||||||
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : controlIndicators.implicitWidth
|
|
||||||
implicitHeight: root.isVerticalOrientation ? controlColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: controlColumn
|
|
||||||
visible: root.isVerticalOrientation
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return "sync"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "lan"
|
|
||||||
}
|
|
||||||
|
|
||||||
return NetworkService.wifiSignalIcon
|
|
||||||
}
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
|
|
||||||
}
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bluetooth"
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: audioIconV.implicitWidth + 4
|
|
||||||
height: audioIconV.implicitHeight + 4
|
|
||||||
color: "transparent"
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: root.showAudioIcon
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: audioIconV
|
|
||||||
|
|
||||||
name: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
|
|
||||||
return "volume_off"
|
|
||||||
} else if (AudioService.sink.audio.volume * 100 < 33) {
|
|
||||||
return "volume_down"
|
|
||||||
} else {
|
|
||||||
return "volume_up"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "volume_up"
|
|
||||||
}
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
onWheel: function(wheelEvent) {
|
|
||||||
let delta = wheelEvent.angleDelta.y
|
|
||||||
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
|
|
||||||
let newVolume
|
|
||||||
if (delta > 0) {
|
|
||||||
newVolume = Math.min(100, currentVolume + 5)
|
|
||||||
} else {
|
|
||||||
newVolume = Math.max(0, currentVolume - 5)
|
|
||||||
}
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.muted = false
|
|
||||||
AudioService.sink.audio.volume = newVolume / 100
|
|
||||||
}
|
|
||||||
wheelEvent.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: controlIndicators
|
|
||||||
visible: !root.isVerticalOrientation
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: networkIcon
|
|
||||||
|
|
||||||
name: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return "sync";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "lan";
|
|
||||||
}
|
|
||||||
|
|
||||||
return NetworkService.wifiSignalIcon;
|
|
||||||
}
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return Theme.primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: bluetoothIcon
|
|
||||||
|
|
||||||
name: "bluetooth"
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: audioIcon.implicitWidth + 4
|
|
||||||
height: audioIcon.implicitHeight + 4
|
|
||||||
color: "transparent"
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showAudioIcon
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: audioIcon
|
|
||||||
|
|
||||||
name: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
|
|
||||||
return "volume_off";
|
|
||||||
} else if (AudioService.sink.audio.volume * 100 < 33) {
|
|
||||||
return "volume_down";
|
|
||||||
} else {
|
|
||||||
return "volume_up";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "volume_up";
|
|
||||||
}
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: audioWheelArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
onWheel: function(wheelEvent) {
|
|
||||||
let delta = wheelEvent.angleDelta.y;
|
|
||||||
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
|
|
||||||
let newVolume;
|
|
||||||
if (delta > 0) {
|
|
||||||
newVolume = Math.min(100, currentVolume + 5);
|
|
||||||
} else {
|
|
||||||
newVolume = Math.max(0, currentVolume - 5);
|
|
||||||
}
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.muted = false;
|
|
||||||
AudioService.sink.audio.volume = newVolume / 100;
|
|
||||||
}
|
|
||||||
wheelEvent.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "mic"
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
onPressed: {
|
|
||||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
|
||||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
|
||||||
const currentScreen = parentScreen || Screen
|
|
||||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
|
||||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
|
||||||
}
|
|
||||||
root.clicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
BasePill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string currentLayout: ""
|
|
||||||
property string hyprlandKeyboard: ""
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth
|
|
||||||
implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
visible: root.isVerticalOrientation
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "keyboard"
|
|
||||||
size: Theme.barIconSize(root.barThickness)
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!root.currentLayout) return ""
|
|
||||||
const parts = root.currentLayout.split(" ")
|
|
||||||
if (parts.length > 0) {
|
|
||||||
return parts[0].substring(0, 2).toUpperCase()
|
|
||||||
}
|
|
||||||
return root.currentLayout.substring(0, 2).toUpperCase()
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness)
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
visible: !root.isVerticalOrientation
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.currentLayout
|
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness)
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
z: 1
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
NiriService.cycleKeyboardLayout()
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
Quickshell.execDetached([
|
|
||||||
"hyprctl",
|
|
||||||
"switchxkblayout",
|
|
||||||
root.hyprlandKeyboard,
|
|
||||||
"next"
|
|
||||||
])
|
|
||||||
updateLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.currentLayout = "Unknown"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(output)
|
|
||||||
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
|
|
||||||
root.hyprlandKeyboard = mainKeyboard.name
|
|
||||||
if (mainKeyboard && mainKeyboard.active_keymap) {
|
|
||||||
root.currentLayout = mainKeyboard.active_keymap
|
|
||||||
} else {
|
|
||||||
root.currentLayout = "Unknown"
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
root.currentLayout = "Unknown"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,766 +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
|
|
||||||
|
|
||||||
property bool isVertical: axis?.isVertical ?? false
|
|
||||||
property var axis: null
|
|
||||||
property string section: "left"
|
|
||||||
property var parentScreen
|
|
||||||
property var hoveredItem: null
|
|
||||||
property var topBar: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
|
||||||
readonly property var sortedToplevels: {
|
|
||||||
const toplevels = CompositorService.sortedToplevels
|
|
||||||
if (!toplevels)
|
|
||||||
return []
|
|
||||||
|
|
||||||
if (SettingsData.runningAppsCurrentWorkspace) {
|
|
||||||
return CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || []
|
|
||||||
}
|
|
||||||
return toplevels
|
|
||||||
}
|
|
||||||
readonly property var groupedWindows: {
|
|
||||||
if (!SettingsData.runningAppsGroupByApp) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (!sortedToplevels || sortedToplevels.length === 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const appGroups = new Map()
|
|
||||||
sortedToplevels.forEach((toplevel, index) => {
|
|
||||||
if (!toplevel)
|
|
||||||
return
|
|
||||||
const appId = toplevel?.appId || "unknown"
|
|
||||||
if (!appGroups.has(appId)) {
|
|
||||||
appGroups.set(appId, {
|
|
||||||
"appId": appId,
|
|
||||||
"windows": []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
appGroups.get(appId).windows.push({
|
|
||||||
"toplevel": toplevel,
|
|
||||||
"windowId": index,
|
|
||||||
"windowTitle": toplevel?.title || "(Unnamed)"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return Array.from(appGroups.values())
|
|
||||||
} catch (e) {
|
|
||||||
console.error("RunningApps: groupedWindows error:", e)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly property int windowCount: SettingsData.runningAppsGroupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
|
|
||||||
readonly property int calculatedSize: {
|
|
||||||
if (windowCount === 0) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (SettingsData.runningAppsCompactMode) {
|
|
||||||
return windowCount * 24 + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
|
|
||||||
} else {
|
|
||||||
return windowCount * (24 + Theme.spacingXS + 120) + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: isVertical ? barThickness : calculatedSize
|
|
||||||
height: isVertical ? calculatedSize : barThickness
|
|
||||||
visible: windowCount > 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualBackground
|
|
||||||
width: root.isVertical ? root.widgetThickness : root.calculatedSize
|
|
||||||
height: root.isVertical ? root.calculatedSize : root.widgetThickness
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
clip: false
|
|
||||||
color: {
|
|
||||||
if (windowCount === 0) {
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsData.dankBarNoBackground) {
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
|
|
||||||
property real scrollAccumulator: 0
|
|
||||||
property real touchpadThreshold: 500
|
|
||||||
|
|
||||||
onWheel: wheel => {
|
|
||||||
const deltaY = wheel.angleDelta.y
|
|
||||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
|
||||||
|
|
||||||
const windows = root.sortedToplevels
|
|
||||||
if (windows.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMouseWheel) {
|
|
||||||
// Direct mouse wheel action
|
|
||||||
let currentIndex = -1
|
|
||||||
for (var i = 0; i < windows.length; i++) {
|
|
||||||
if (windows[i].activated) {
|
|
||||||
currentIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextIndex
|
|
||||||
if (deltaY < 0) {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = 0
|
|
||||||
} else {
|
|
||||||
nextIndex = (currentIndex + 1) % windows.length
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = windows.length - 1
|
|
||||||
} else {
|
|
||||||
nextIndex = (currentIndex - 1 + windows.length) % windows.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextWindow = windows[nextIndex]
|
|
||||||
if (nextWindow) {
|
|
||||||
nextWindow.activate()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Touchpad - accumulate small deltas
|
|
||||||
scrollAccumulator += deltaY
|
|
||||||
|
|
||||||
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
|
|
||||||
let currentIndex = -1
|
|
||||||
for (var i = 0; i < windows.length; i++) {
|
|
||||||
if (windows[i].activated) {
|
|
||||||
currentIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextIndex
|
|
||||||
if (scrollAccumulator < 0) {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = 0
|
|
||||||
} else {
|
|
||||||
nextIndex = (currentIndex + 1) % windows.length
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = windows.length - 1
|
|
||||||
} else {
|
|
||||||
nextIndex = (currentIndex - 1 + windows.length) % windows.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextWindow = windows[nextIndex]
|
|
||||||
if (nextWindow) {
|
|
||||||
nextWindow.activate()
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollAccumulator = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wheel.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: layoutLoader
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: root.isVertical ? columnLayout : rowLayout
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: rowLayout
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: windowRepeater
|
|
||||||
model: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateItem
|
|
||||||
|
|
||||||
property bool isGrouped: SettingsData.runningAppsGroupByApp
|
|
||||||
property var groupData: isGrouped ? modelData : null
|
|
||||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
|
||||||
property bool isFocused: toplevelData ? toplevelData.activated : false
|
|
||||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
|
||||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
|
||||||
property var toplevelObject: toplevelData
|
|
||||||
property int windowCount: isGrouped ? modelData.windows.length : 1
|
|
||||||
property string tooltipText: {
|
|
||||||
let appName = "Unknown"
|
|
||||||
if (appId) {
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
|
||||||
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
|
|
||||||
}
|
|
||||||
if (isGrouped && windowCount > 1) {
|
|
||||||
return appName + " (" + windowCount + " windows)"
|
|
||||||
}
|
|
||||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
|
||||||
}
|
|
||||||
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
|
|
||||||
|
|
||||||
width: visualWidth
|
|
||||||
height: root.barThickness
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: delegateItem.visualWidth
|
|
||||||
height: 24
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (isFocused) {
|
|
||||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
|
||||||
} else {
|
|
||||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// App icon
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - 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
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
|
|
||||||
anchors.bottomMargin: -2
|
|
||||||
width: 14
|
|
||||||
height: 14
|
|
||||||
radius: 7
|
|
||||||
color: Theme.primary
|
|
||||||
visible: isGrouped && windowCount > 1
|
|
||||||
z: 10
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: windowCount > 9 ? "9+" : windowCount
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: Theme.surface
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Window title text (only visible in expanded mode)
|
|
||||||
StyledText {
|
|
||||||
anchors.left: iconImg.right
|
|
||||||
anchors.leftMargin: Theme.spacingXS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !SettingsData.runningAppsCompactMode
|
|
||||||
text: windowTitle
|
|
||||||
font.pixelSize: Theme.barTextSize(barThickness)
|
|
||||||
color: Theme.surfaceText
|
|
||||||
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 (isGrouped && windowCount > 1) {
|
|
||||||
let currentIndex = -1
|
|
||||||
for (var i = 0; i < groupData.windows.length; i++) {
|
|
||||||
if (groupData.windows[i].toplevel.activated) {
|
|
||||||
currentIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const nextIndex = (currentIndex + 1) % groupData.windows.length
|
|
||||||
groupData.windows[nextIndex].toplevel.activate()
|
|
||||||
} else if (toplevelObject) {
|
|
||||||
toplevelObject.activate()
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hide()
|
|
||||||
}
|
|
||||||
tooltipLoader.active = false
|
|
||||||
|
|
||||||
windowContextMenuLoader.active = true
|
|
||||||
if (windowContextMenuLoader.item) {
|
|
||||||
windowContextMenuLoader.item.currentWindow = toplevelObject
|
|
||||||
if (root.isVertical) {
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
|
||||||
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
|
|
||||||
} else {
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
const relativeX = globalPos.x - screenX
|
|
||||||
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
|
|
||||||
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onEntered: {
|
|
||||||
root.hoveredItem = delegateItem
|
|
||||||
tooltipLoader.active = true
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
if (root.isVertical) {
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
|
||||||
const isLeft = root.axis?.edge === "left"
|
|
||||||
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
|
||||||
} else {
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
|
|
||||||
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
|
|
||||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
if (root.hoveredItem === delegateItem) {
|
|
||||||
root.hoveredItem = null
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltipLoader.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: columnLayout
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: windowRepeater
|
|
||||||
model: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateItem
|
|
||||||
|
|
||||||
property bool isGrouped: SettingsData.runningAppsGroupByApp
|
|
||||||
property var groupData: isGrouped ? modelData : null
|
|
||||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
|
||||||
property bool isFocused: toplevelData ? toplevelData.activated : false
|
|
||||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
|
||||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
|
||||||
property var toplevelObject: toplevelData
|
|
||||||
property int windowCount: isGrouped ? modelData.windows.length : 1
|
|
||||||
property string tooltipText: {
|
|
||||||
let appName = "Unknown"
|
|
||||||
if (appId) {
|
|
||||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
|
||||||
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
|
|
||||||
}
|
|
||||||
if (isGrouped && windowCount > 1) {
|
|
||||||
return appName + " (" + windowCount + " windows)"
|
|
||||||
}
|
|
||||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
|
||||||
}
|
|
||||||
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
|
|
||||||
|
|
||||||
width: root.barThickness
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: delegateItem.visualWidth
|
|
||||||
height: 24
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (isFocused) {
|
|
||||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
|
||||||
} else {
|
|
||||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
|
|
||||||
anchors.bottomMargin: -2
|
|
||||||
width: 14
|
|
||||||
height: 14
|
|
||||||
radius: 7
|
|
||||||
color: Theme.primary
|
|
||||||
visible: isGrouped && windowCount > 1
|
|
||||||
z: 10
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: windowCount > 9 ? "9+" : windowCount
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: Theme.surface
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.left: iconImg.right
|
|
||||||
anchors.leftMargin: Theme.spacingXS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !SettingsData.runningAppsCompactMode
|
|
||||||
text: windowTitle
|
|
||||||
font.pixelSize: Theme.barTextSize(barThickness)
|
|
||||||
color: Theme.surfaceText
|
|
||||||
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 (isGrouped && windowCount > 1) {
|
|
||||||
let currentIndex = -1
|
|
||||||
for (var i = 0; i < groupData.windows.length; i++) {
|
|
||||||
if (groupData.windows[i].toplevel.activated) {
|
|
||||||
currentIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const nextIndex = (currentIndex + 1) % groupData.windows.length
|
|
||||||
groupData.windows[nextIndex].toplevel.activate()
|
|
||||||
} else if (toplevelObject) {
|
|
||||||
toplevelObject.activate()
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hide()
|
|
||||||
}
|
|
||||||
tooltipLoader.active = false
|
|
||||||
|
|
||||||
windowContextMenuLoader.active = true
|
|
||||||
if (windowContextMenuLoader.item) {
|
|
||||||
windowContextMenuLoader.item.currentWindow = toplevelObject
|
|
||||||
if (root.isVertical) {
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
|
||||||
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
|
|
||||||
} else {
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
const relativeX = globalPos.x - screenX
|
|
||||||
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
|
|
||||||
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onEntered: {
|
|
||||||
root.hoveredItem = delegateItem
|
|
||||||
tooltipLoader.active = true
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
if (root.isVertical) {
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
|
||||||
const isLeft = root.axis?.edge === "left"
|
|
||||||
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
|
||||||
} else {
|
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
|
|
||||||
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
|
|
||||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
if (root.hoveredItem === delegateItem) {
|
|
||||||
root.hoveredItem = null
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltipLoader.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: tooltipLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
sourceComponent: DankTooltip {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: windowContextMenuLoader
|
|
||||||
active: false
|
|
||||||
sourceComponent: PanelWindow {
|
|
||||||
id: contextMenuWindow
|
|
||||||
|
|
||||||
property var currentWindow: null
|
|
||||||
property bool isVisible: false
|
|
||||||
property point anchorPos: Qt.point(0, 0)
|
|
||||||
property bool isVertical: false
|
|
||||||
property string edge: "top"
|
|
||||||
|
|
||||||
function showAt(x, y, vertical, barEdge) {
|
|
||||||
screen = root.parentScreen
|
|
||||||
anchorPos = Qt.point(x, y)
|
|
||||||
isVertical = vertical ?? false
|
|
||||||
edge = barEdge ?? "top"
|
|
||||||
isVisible = true
|
|
||||||
visible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
isVisible = false
|
|
||||||
visible = false
|
|
||||||
windowContextMenuLoader.active = false
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitWidth: 100
|
|
||||||
implicitHeight: 40
|
|
||||||
visible: false
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: contextMenuWindow.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
x: {
|
|
||||||
if (contextMenuWindow.isVertical) {
|
|
||||||
if (contextMenuWindow.edge === "left") {
|
|
||||||
return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x)
|
|
||||||
} else {
|
|
||||||
return Math.max(10, contextMenuWindow.anchorPos.x - width)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const left = 10
|
|
||||||
const right = contextMenuWindow.width - width - 10
|
|
||||||
const want = contextMenuWindow.anchorPos.x - width / 2
|
|
||||||
return Math.max(left, Math.min(right, want))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (contextMenuWindow.isVertical) {
|
|
||||||
const top = 10
|
|
||||||
const bottom = contextMenuWindow.height - height - 10
|
|
||||||
const want = contextMenuWindow.anchorPos.y - height / 2
|
|
||||||
return Math.max(top, Math.min(bottom, want))
|
|
||||||
} else {
|
|
||||||
return contextMenuWindow.anchorPos.y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width: 100
|
|
||||||
height: 32
|
|
||||||
color: Theme.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: I18n.tr("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,661 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.SystemTray
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isVertical: axis?.isVertical ?? false
|
|
||||||
property var axis: null
|
|
||||||
property var parentWindow: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
property bool isAtBottom: false
|
|
||||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
readonly property var hiddenTrayIds: {
|
|
||||||
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || ""
|
|
||||||
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : []
|
|
||||||
}
|
|
||||||
readonly property var visibleTrayItems: {
|
|
||||||
if (!hiddenTrayIds.length) {
|
|
||||||
return SystemTray.items.values
|
|
||||||
}
|
|
||||||
return SystemTray.items.values.filter(item => {
|
|
||||||
const itemId = item?.id || ""
|
|
||||||
return !hiddenTrayIds.includes(itemId.toLowerCase())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
readonly property int calculatedSize: visibleTrayItems.length > 0 ? visibleTrayItems.length * 24 + horizontalPadding * 2 : 0
|
|
||||||
readonly property real visualWidth: isVertical ? widgetThickness : calculatedSize
|
|
||||||
readonly property real visualHeight: isVertical ? calculatedSize : widgetThickness
|
|
||||||
|
|
||||||
width: isVertical ? barThickness : visualWidth
|
|
||||||
height: isVertical ? visualHeight : barThickness
|
|
||||||
visible: visibleTrayItems.length > 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualBackground
|
|
||||||
width: root.visualWidth
|
|
||||||
height: root.visualHeight
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (visibleTrayItems.length === 0) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsData.dankBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: layoutLoader
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: root.isVertical ? columnComp : rowComp
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: rowComp
|
|
||||||
Row {
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.visibleTrayItems
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateRoot
|
|
||||||
property var trayItem: modelData
|
|
||||||
property string iconSource: {
|
|
||||||
let icon = trayItem && trayItem.icon;
|
|
||||||
if (typeof icon === 'string' || icon instanceof String) {
|
|
||||||
if (icon === "") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const split = icon.split("?path=");
|
|
||||||
if (split.length !== 2) {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = split[0];
|
|
||||||
const path = split[1];
|
|
||||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
if (fileName.startsWith("dropboxstatus")) {
|
|
||||||
fileName = `hicolor/16x16/status/${fileName}`;
|
|
||||||
}
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
if (icon.startsWith("/") && !icon.startsWith("file://")) {
|
|
||||||
return `file://${icon}`;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 24
|
|
||||||
height: root.barThickness
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
source: delegateRoot.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 (!delegateRoot.trayItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton && !delegateRoot.trayItem.onlyMenu) {
|
|
||||||
delegateRoot.trayItem.activate();
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (delegateRoot.trayItem.hasMenu) {
|
|
||||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: columnComp
|
|
||||||
Column {
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.visibleTrayItems
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateRoot
|
|
||||||
property var trayItem: modelData
|
|
||||||
property string iconSource: {
|
|
||||||
let icon = trayItem && trayItem.icon;
|
|
||||||
if (typeof icon === 'string' || icon instanceof String) {
|
|
||||||
if (icon === "") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const split = icon.split("?path=");
|
|
||||||
if (split.length !== 2) {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = split[0];
|
|
||||||
const path = split[1];
|
|
||||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
if (fileName.startsWith("dropboxstatus")) {
|
|
||||||
fileName = `hicolor/16x16/status/${fileName}`;
|
|
||||||
}
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
if (icon.startsWith("/") && !icon.startsWith("file://")) {
|
|
||||||
return `file://${icon}`;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
width: root.barThickness
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
source: delegateRoot.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 (!delegateRoot.trayItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton && !delegateRoot.trayItem.onlyMenu) {
|
|
||||||
delegateRoot.trayItem.activate();
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (delegateRoot.trayItem.hasMenu) {
|
|
||||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: trayMenuComponent
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: menuRoot
|
|
||||||
|
|
||||||
property var trayItem: null
|
|
||||||
property var anchorItem: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property bool isAtBottom: false
|
|
||||||
property bool isVertical: false
|
|
||||||
property var axis: null
|
|
||||||
property bool showMenu: false
|
|
||||||
property var menuHandle: null
|
|
||||||
|
|
||||||
ListModel { id: entryStack }
|
|
||||||
function topEntry() {
|
|
||||||
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null
|
|
||||||
}
|
|
||||||
|
|
||||||
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
|
||||||
trayItem = item
|
|
||||||
anchorItem = anchor
|
|
||||||
parentScreen = screen
|
|
||||||
isAtBottom = atBottom
|
|
||||||
isVertical = vertical
|
|
||||||
axis = axisObj
|
|
||||||
menuHandle = item?.menu
|
|
||||||
|
|
||||||
if (parentScreen) {
|
|
||||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
|
||||||
const s = Quickshell.screens[i]
|
|
||||||
if (s === parentScreen) {
|
|
||||||
menuWindow.screen = s
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showMenu = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSubMenu(entry) {
|
|
||||||
if (!entry || !entry.hasChildren) return;
|
|
||||||
|
|
||||||
entryStack.append({ handle: entry });
|
|
||||||
|
|
||||||
const h = entry.menu || entry;
|
|
||||||
if (h && typeof h.updateLayout === "function") h.updateLayout();
|
|
||||||
|
|
||||||
submenuHydrator.menu = h;
|
|
||||||
submenuHydrator.open();
|
|
||||||
Qt.callLater(() => submenuHydrator.close());
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
if (!entryStack.count) return;
|
|
||||||
entryStack.remove(entryStack.count - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 0
|
|
||||||
height: 0
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: menuWindow
|
|
||||||
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
updatePosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePosition() {
|
|
||||||
if (!menuRoot.anchorItem || !menuRoot.trayItem) {
|
|
||||||
anchorPos = Qt.point(screen.width / 2, screen.height / 2)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalPos = menuRoot.anchorItem.mapToGlobal(0, 0)
|
|
||||||
const screenX = screen.x || 0
|
|
||||||
const screenY = screen.y || 0
|
|
||||||
const relativeX = globalPos.x - screenX
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
|
|
||||||
const widgetThickness = Math.max(20, 26 + SettingsData.dankBarInnerPadding * 0.6)
|
|
||||||
const effectiveBarThickness = Math.max(widgetThickness + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding))
|
|
||||||
|
|
||||||
if (menuRoot.isVertical) {
|
|
||||||
const edge = menuRoot.axis?.edge
|
|
||||||
let targetX
|
|
||||||
if (edge === "left") {
|
|
||||||
targetX = effectiveBarThickness + SettingsData.dankBarSpacing + Theme.popupDistance
|
|
||||||
} else {
|
|
||||||
const popupX = effectiveBarThickness + SettingsData.dankBarSpacing + Theme.popupDistance
|
|
||||||
targetX = screen.width - popupX
|
|
||||||
}
|
|
||||||
anchorPos = Qt.point(targetX, relativeY + menuRoot.anchorItem.height / 2)
|
|
||||||
} else {
|
|
||||||
let targetY
|
|
||||||
if (menuRoot.isAtBottom) {
|
|
||||||
const popupY = effectiveBarThickness + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap + Theme.popupDistance
|
|
||||||
targetY = screen.height - popupY
|
|
||||||
} else {
|
|
||||||
targetY = effectiveBarThickness + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap + Theme.popupDistance
|
|
||||||
}
|
|
||||||
anchorPos = Qt.point(relativeX + menuRoot.anchorItem.width / 2, targetY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: menuContainer
|
|
||||||
|
|
||||||
width: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
|
|
||||||
height: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
|
|
||||||
|
|
||||||
x: {
|
|
||||||
if (menuRoot.isVertical) {
|
|
||||||
const edge = menuRoot.axis?.edge
|
|
||||||
if (edge === "left") {
|
|
||||||
const targetX = menuWindow.anchorPos.x
|
|
||||||
return Math.min(menuWindow.screen.width - width - 10, targetX)
|
|
||||||
} else {
|
|
||||||
const targetX = menuWindow.anchorPos.x - width
|
|
||||||
return Math.max(10, targetX)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const left = 10
|
|
||||||
const right = menuWindow.width - width - 10
|
|
||||||
const want = menuWindow.anchorPos.x - width / 2
|
|
||||||
return Math.max(left, Math.min(right, want))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
y: {
|
|
||||||
if (menuRoot.isVertical) {
|
|
||||||
const top = 10
|
|
||||||
const bottom = menuWindow.height - height - 10
|
|
||||||
const want = menuWindow.anchorPos.y - height / 2
|
|
||||||
return Math.max(top, Math.min(bottom, want))
|
|
||||||
} else {
|
|
||||||
if (menuRoot.isAtBottom) {
|
|
||||||
const targetY = menuWindow.anchorPos.y - height
|
|
||||||
return Math.max(10, targetY)
|
|
||||||
} else {
|
|
||||||
const targetY = menuWindow.anchorPos.y
|
|
||||||
return Math.min(menuWindow.screen.height - height - 10, targetY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
opacity: menuRoot.showMenu ? 1 : 0
|
|
||||||
scale: menuRoot.showMenu ? 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
|
|
||||||
}
|
|
||||||
|
|
||||||
QsMenuAnchor {
|
|
||||||
id: submenuHydrator
|
|
||||||
anchor.window: menuWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
QsMenuOpener {
|
|
||||||
id: rootOpener
|
|
||||||
menu: menuRoot.menuHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
QsMenuOpener {
|
|
||||||
id: subOpener
|
|
||||||
menu: {
|
|
||||||
const e = menuRoot.topEntry();
|
|
||||||
return e ? (e.menu || e) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: entryStack.count > 0
|
|
||||||
width: parent.width
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: backArea.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.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "arrow_back"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Back")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: backArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: menuRoot.goBack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: entryStack.count > 0
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: entryStack.count
|
|
||||||
? (subOpener.children ? subOpener.children
|
|
||||||
: (menuRoot.topEntry()?.children || []))
|
|
||||||
: rootOpener.children
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property var menuEntry: modelData
|
|
||||||
|
|
||||||
width: menuColumn.width
|
|
||||||
height: menuEntry?.isSeparator ? 1 : 28
|
|
||||||
radius: menuEntry?.isSeparator ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (menuEntry?.isSeparator) {
|
|
||||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
return itemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: itemArea
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: !menuEntry?.isSeparator && (menuEntry?.enabled !== false)
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (!menuEntry || menuEntry.isSeparator) return;
|
|
||||||
|
|
||||||
if (menuEntry.hasChildren) {
|
|
||||||
menuRoot.showSubMenu(menuEntry);
|
|
||||||
} else {
|
|
||||||
if (typeof menuEntry.activate === "function") {
|
|
||||||
menuEntry.activate();
|
|
||||||
} else if (typeof menuEntry.triggered === "function") {
|
|
||||||
menuEntry.triggered();
|
|
||||||
}
|
|
||||||
Qt.createQmlObject('import QtQuick; Timer { interval: 80; running: true; repeat: false; onTriggered: menuRoot.close() }', menuRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: !menuEntry?.isSeparator
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0
|
|
||||||
radius: menuEntry?.buttonType === 2 ? 8 : 2
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outline
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - 6
|
|
||||||
height: parent.height - 6
|
|
||||||
radius: parent.radius - 3
|
|
||||||
color: Theme.primary
|
|
||||||
visible: menuEntry?.checkState === 2
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "check"
|
|
||||||
size: 10
|
|
||||||
color: Theme.primaryText
|
|
||||||
visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: menuEntry?.icon && menuEntry.icon !== ""
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: menuEntry?.icon || ""
|
|
||||||
sourceSize.width: 16
|
|
||||||
sourceSize.height: 16
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
smooth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: menuEntry?.text || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: (menuEntry?.enabled !== false) ? Theme.surfaceText : Theme.surfaceTextMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Math.max(150, parent.width - 64)
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "chevron_right"
|
|
||||||
size: 14
|
|
||||||
color: Theme.surfaceText
|
|
||||||
visible: menuEntry?.hasChildren ?? false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
z: -1
|
|
||||||
onClicked: menuRoot.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var currentTrayMenu: null
|
|
||||||
|
|
||||||
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
|
||||||
if (currentTrayMenu) {
|
|
||||||
currentTrayMenu.destroy()
|
|
||||||
}
|
|
||||||
currentTrayMenu = trayMenuComponent.createObject(null)
|
|
||||||
if (currentTrayMenu) {
|
|
||||||
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
BasePill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: VpnService
|
|
||||||
}
|
|
||||||
|
|
||||||
property var popoutTarget: null
|
|
||||||
|
|
||||||
signal toggleVpnPopup()
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
|
|
||||||
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: icon
|
|
||||||
|
|
||||||
name: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off")
|
|
||||||
size: Theme.barIconSize(root.barThickness, -4)
|
|
||||||
color: VpnService.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: tooltipLoader
|
|
||||||
active: false
|
|
||||||
sourceComponent: DankTooltip {}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clickArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
onPressed: {
|
|
||||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
|
||||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
|
||||||
const currentScreen = parentScreen || Screen
|
|
||||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
|
||||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
|
||||||
}
|
|
||||||
root.toggleVpnPopup();
|
|
||||||
}
|
|
||||||
onEntered: {
|
|
||||||
if (root.parentScreen && !(popoutTarget && popoutTarget.shouldBeVisible)) {
|
|
||||||
tooltipLoader.active = true
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
let tooltipText = ""
|
|
||||||
if (!VpnService.connected) {
|
|
||||||
tooltipText = "VPN Disconnected"
|
|
||||||
} else {
|
|
||||||
const names = VpnService.activeNames || []
|
|
||||||
if (names.length <= 1) {
|
|
||||||
tooltipText = "VPN Connected • " + (names[0] || "")
|
|
||||||
} else {
|
|
||||||
tooltipText = "VPN Connected • " + names[0] + " +" + (names.length - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.isVerticalOrientation) {
|
|
||||||
const globalPos = mapToGlobal(width / 2, height / 2)
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
|
||||||
const isLeft = root.axis?.edge === "left"
|
|
||||||
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
|
||||||
} else {
|
|
||||||
const globalPos = mapToGlobal(width / 2, height)
|
|
||||||
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
|
|
||||||
tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hide()
|
|
||||||
}
|
|
||||||
tooltipLoader.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,757 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isVertical: axis?.isVertical ?? false
|
|
||||||
property var axis: null
|
|
||||||
property string screenName: ""
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
property var hyprlandOverviewLoader: null
|
|
||||||
property var parentScreen: null
|
|
||||||
readonly property var sortedToplevels: {
|
|
||||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen?.name);
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
// Filter out special workspaces
|
|
||||||
const filteredList = baseList.filter(ws => ws.id > -1)
|
|
||||||
return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList
|
|
||||||
}
|
|
||||||
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: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
|
|
||||||
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
|
|
||||||
|
|
||||||
function getRealWorkspaces() {
|
|
||||||
return root.workspaceList.filter(ws => {
|
|
||||||
if (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: isVertical ? barThickness : visualWidth
|
|
||||||
height: isVertical ? visualHeight : barThickness
|
|
||||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualBackground
|
|
||||||
width: root.visualWidth
|
|
||||||
height: root.visualHeight
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.dankBarNoBackground)
|
|
||||||
return "transparent"
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
|
|
||||||
property real scrollAccumulator: 0
|
|
||||||
property real touchpadThreshold: 500
|
|
||||||
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.RightButton && CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (!SettingsData.workspaceScrolling || !CompositorService.isNiri) {
|
|
||||||
switchWorkspace(direction)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const windows = root.sortedToplevels;
|
|
||||||
if (windows.length < 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let currentIndex = -1;
|
|
||||||
for (let i = 0; i < windows.length; i++) {
|
|
||||||
if (windows[i].activated) {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
let nextIndex;
|
|
||||||
if (deltaY < 0) {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = 0;
|
|
||||||
} else {
|
|
||||||
nextIndex = currentIndex +1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = windows.length -1;
|
|
||||||
} else {
|
|
||||||
nextIndex = currentIndex - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const nextWindow = windows[nextIndex];
|
|
||||||
if (nextWindow) {
|
|
||||||
nextWindow.activate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
scrollAccumulator += deltaY
|
|
||||||
|
|
||||||
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
|
|
||||||
const touchDirection = scrollAccumulator < 0 ? 1 : -1
|
|
||||||
if (!SettingsData.workspaceScrolling || !CompositorService.isNiri) {
|
|
||||||
switchWorkspace(touchDirection)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const windows = root.sortedToplevels;
|
|
||||||
if (windows.length < 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let currentIndex = -1;
|
|
||||||
for (let i = 0; i < windows.length; i++) {
|
|
||||||
if (windows[i].activated) {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
let nextIndex;
|
|
||||||
if (deltaY < 0) {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = 0;
|
|
||||||
} else {
|
|
||||||
nextIndex = currentIndex +1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
nextIndex = windows.length -1;
|
|
||||||
} else {
|
|
||||||
nextIndex = currentIndex - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const nextWindow = windows[nextIndex];
|
|
||||||
if (nextWindow) {
|
|
||||||
nextWindow.activate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollAccumulator = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wheel.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
id: workspaceRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.workspaceList
|
|
||||||
|
|
||||||
Item {
|
|
||||||
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 bool loadedIsUrgent: false
|
|
||||||
property bool isUrgent: {
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return modelData?.urgent ?? false
|
|
||||||
}
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
return loadedIsUrgent
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
property var loadedIconData: null
|
|
||||||
property bool loadedHasIcon: false
|
|
||||||
property var loadedIcons: []
|
|
||||||
|
|
||||||
readonly property real visualWidth: {
|
|
||||||
if (root.isVertical) {
|
|
||||||
return SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5
|
|
||||||
} else {
|
|
||||||
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 * 0.9 + Theme.spacingXS : root.widgetHeight * 0.7
|
|
||||||
return baseWidth + iconsWidth
|
|
||||||
}
|
|
||||||
return isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly property real visualHeight: {
|
|
||||||
if (root.isVertical) {
|
|
||||||
if (SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
|
||||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
|
|
||||||
const iconsHeight = numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0)
|
|
||||||
const baseHeight = isActive ? root.widgetHeight * 0.9 + Theme.spacingXS : root.widgetHeight * 0.7
|
|
||||||
return baseHeight + iconsHeight
|
|
||||||
}
|
|
||||||
return isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7
|
|
||||||
} else {
|
|
||||||
return SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: dataUpdateTimer
|
|
||||||
interval: 50
|
|
||||||
onTriggered: {
|
|
||||||
if (isPlaceholder) {
|
|
||||||
delegateRoot.loadedWorkspaceData = null
|
|
||||||
delegateRoot.loadedIconData = null
|
|
||||||
delegateRoot.loadedHasIcon = false
|
|
||||||
delegateRoot.loadedIcons = []
|
|
||||||
delegateRoot.loadedIsUrgent = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var wsData = null;
|
|
||||||
if (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;
|
|
||||||
delegateRoot.loadedIsUrgent = wsData?.is_urgent ?? false;
|
|
||||||
|
|
||||||
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: root.isVertical ? root.barThickness : visualWidth
|
|
||||||
height: root.isVertical ? visualHeight : root.barThickness
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: delegateRoot.visualWidth
|
|
||||||
height: delegateRoot.visualHeight
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
|
|
||||||
|
|
||||||
border.width: isUrgent && !isActive ? 2 : 0
|
|
||||||
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
enabled: (!SettingsData.showWorkspaceApps || SettingsData.maxWorkspaceIcons <= 3)
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
enabled: root.isVertical && (!SettingsData.showWorkspaceApps || SettingsData.maxWorkspaceIcons <= 3)
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: appIconsLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
Loader {
|
|
||||||
id: contentRow
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: root.isVertical ? columnLayout : rowLayout
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: rowLayout
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
visible: loadedIcons.length > 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: columnLayout
|
|
||||||
Column {
|
|
||||||
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.barTextSize(barThickness)
|
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loader for Workspace Index
|
|
||||||
Loader {
|
|
||||||
id: indexLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: !isPlaceholder && SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
const isPlaceholder = CompositorService.isHyprland ? (modelData?.id === -1) : (modelData === -1)
|
|
||||||
if (isPlaceholder) {
|
|
||||||
return index + 1
|
|
||||||
}
|
|
||||||
return CompositorService.isHyprland ? (modelData?.id || "") : (modelData - 1);
|
|
||||||
}
|
|
||||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
|
||||||
font.pixelSize: Theme.barTextSize(barThickness)
|
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: updateAllData()
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: CompositorService
|
|
||||||
function onSortedToplevelsChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: NiriService
|
|
||||||
enabled: CompositorService.isNiri
|
|
||||||
function onAllWorkspacesChanged() { delegateRoot.updateAllData() }
|
|
||||||
function onWindowUrgentChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() }
|
|
||||||
function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,299 +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 var triggerScreen: null
|
|
||||||
property int currentTabIndex: 0
|
|
||||||
|
|
||||||
keyboardFocusMode: WlrKeyboardFocus.Exclusive
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
triggerY = y
|
|
||||||
|
|
||||||
if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)) {
|
|
||||||
const screenWidth = screen ? screen.width : Screen.width
|
|
||||||
triggerX = (screenWidth - popupWidth) / 2
|
|
||||||
triggerWidth = popupWidth
|
|
||||||
} else if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)) {
|
|
||||||
const screenHeight = screen ? screen.height : Screen.height
|
|
||||||
triggerX = (screenHeight - popupHeight) / 2
|
|
||||||
triggerWidth = popupHeight
|
|
||||||
} else {
|
|
||||||
triggerX = x
|
|
||||||
triggerWidth = width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popupWidth: 700
|
|
||||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
|
||||||
triggerX: Screen.width - 620 - Theme.spacingL
|
|
||||||
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
|
|
||||||
triggerWidth: 80
|
|
||||||
shouldBeVisible: dashVisible
|
|
||||||
visible: shouldBeVisible
|
|
||||||
|
|
||||||
property bool __focusArmed: false
|
|
||||||
property bool __contentReady: false
|
|
||||||
|
|
||||||
function __tryFocusOnce() {
|
|
||||||
if (!__focusArmed) return
|
|
||||||
const win = root.window
|
|
||||||
if (!win || !win.visible) return
|
|
||||||
if (!contentLoader.item) return
|
|
||||||
|
|
||||||
if (win.requestActivate) win.requestActivate()
|
|
||||||
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
|
|
||||||
|
|
||||||
if (contentLoader.item.activeFocus)
|
|
||||||
__focusArmed = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onDashVisibleChanged: {
|
|
||||||
if (dashVisible) {
|
|
||||||
__focusArmed = true
|
|
||||||
__contentReady = !!contentLoader.item
|
|
||||||
open()
|
|
||||||
__tryFocusOnce()
|
|
||||||
} else {
|
|
||||||
__focusArmed = false
|
|
||||||
__contentReady = false
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: contentLoader
|
|
||||||
function onLoaded() {
|
|
||||||
__contentReady = true
|
|
||||||
if (__focusArmed) __tryFocusOnce()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root.window ? root.window : null
|
|
||||||
enabled: !!root.window
|
|
||||||
function onVisibleChanged() { if (__focusArmed) __tryFocusOnce() }
|
|
||||||
}
|
|
||||||
|
|
||||||
onBackgroundClicked: {
|
|
||||||
dashVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Rectangle {
|
|
||||||
id: mainContainer
|
|
||||||
|
|
||||||
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
mainContainer.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
Qt.callLater(function() {
|
|
||||||
mainContainer.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: function(event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
root.dashVisible = false
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
|
|
||||||
let nextIndex = root.currentTabIndex + 1
|
|
||||||
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
|
|
||||||
nextIndex++
|
|
||||||
}
|
|
||||||
if (nextIndex >= tabBar.model.length) {
|
|
||||||
nextIndex = 0
|
|
||||||
}
|
|
||||||
root.currentTabIndex = nextIndex
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
|
|
||||||
let prevIndex = root.currentTabIndex - 1
|
|
||||||
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
|
|
||||||
prevIndex--
|
|
||||||
}
|
|
||||||
if (prevIndex < 0) {
|
|
||||||
prevIndex = tabBar.model.length - 1
|
|
||||||
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
|
|
||||||
prevIndex--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (prevIndex >= 0) {
|
|
||||||
root.currentTabIndex = prevIndex
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
|
|
||||||
if (wallpaperTab.handleKeyEvent(event)) {
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
|
||||||
radius: parent.radius
|
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
|
||||||
running: root.shouldBeVisible
|
|
||||||
loops: Animation.Infinite
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
to: 0.08
|
|
||||||
duration: Theme.extraLongDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
to: 0.02
|
|
||||||
duration: Theme.extraLongDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankTabBar {
|
|
||||||
id: tabBar
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 48
|
|
||||||
currentIndex: root.currentTabIndex
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
equalWidthTabs: true
|
|
||||||
enableArrowNavigation: false
|
|
||||||
focus: false
|
|
||||||
activeFocusOnTab: false
|
|
||||||
nextFocusTarget: {
|
|
||||||
const item = pages.currentItem
|
|
||||||
if (!item)
|
|
||||||
return null
|
|
||||||
if (item.focusTarget)
|
|
||||||
return item.focusTarget
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
model: {
|
|
||||||
let tabs = [
|
|
||||||
{ icon: "dashboard", text: I18n.tr("Overview") },
|
|
||||||
{ icon: "music_note", text: I18n.tr("Media") },
|
|
||||||
{ icon: "wallpaper", text: I18n.tr("Wallpapers") }
|
|
||||||
]
|
|
||||||
|
|
||||||
if (SettingsData.weatherEnabled) {
|
|
||||||
tabs.push({ icon: "wb_sunny", text: I18n.tr("Weather") })
|
|
||||||
}
|
|
||||||
|
|
||||||
tabs.push({ icon: "settings", text: I18n.tr("Settings"), isAction: true })
|
|
||||||
return tabs
|
|
||||||
}
|
|
||||||
|
|
||||||
onTabClicked: function(index) {
|
|
||||||
root.currentTabIndex = index
|
|
||||||
}
|
|
||||||
|
|
||||||
onActionTriggered: function(index) {
|
|
||||||
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
|
|
||||||
if (index === settingsIndex) {
|
|
||||||
dashVisible = false
|
|
||||||
settingsModal.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Theme.spacingXS
|
|
||||||
}
|
|
||||||
|
|
||||||
StackLayout {
|
|
||||||
id: pages
|
|
||||||
width: parent.width
|
|
||||||
implicitHeight: {
|
|
||||||
if (currentIndex === 0) return overviewTab.implicitHeight
|
|
||||||
if (currentIndex === 1) return mediaTab.implicitHeight
|
|
||||||
if (currentIndex === 2) return wallpaperTab.implicitHeight
|
|
||||||
if (SettingsData.weatherEnabled && currentIndex === 3) return weatherTab.implicitHeight
|
|
||||||
return overviewTab.implicitHeight
|
|
||||||
}
|
|
||||||
currentIndex: root.currentTabIndex
|
|
||||||
|
|
||||||
OverviewTab {
|
|
||||||
id: overviewTab
|
|
||||||
|
|
||||||
onSwitchToWeatherTab: {
|
|
||||||
if (SettingsData.weatherEnabled) {
|
|
||||||
tabBar.currentIndex = 3
|
|
||||||
tabBar.tabClicked(3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSwitchToMediaTab: {
|
|
||||||
tabBar.currentIndex = 1
|
|
||||||
tabBar.tabClicked(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaPlayerTab {
|
|
||||||
id: mediaTab
|
|
||||||
}
|
|
||||||
|
|
||||||
WallpaperTab {
|
|
||||||
id: wallpaperTab
|
|
||||||
active: root.currentTabIndex === 2
|
|
||||||
tabBarItem: tabBar
|
|
||||||
keyForwardTarget: mainContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
WeatherTab {
|
|
||||||
id: weatherTab
|
|
||||||
visible: SettingsData.weatherEnabled && root.currentTabIndex === 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,523 +0,0 @@
|
|||||||
import Qt.labs.folderlistmodel
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.FileBrowser
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
implicitWidth: 700
|
|
||||||
implicitHeight: 410
|
|
||||||
|
|
||||||
property var wallpaperList: []
|
|
||||||
property string wallpaperDir: ""
|
|
||||||
property int currentPage: 0
|
|
||||||
property int itemsPerPage: 16
|
|
||||||
property int totalPages: Math.max(1, Math.ceil(wallpaperList.length / itemsPerPage))
|
|
||||||
property bool active: false
|
|
||||||
property Item focusTarget: wallpaperGrid
|
|
||||||
property Item tabBarItem: null
|
|
||||||
property int gridIndex: 0
|
|
||||||
property Item keyForwardTarget: null
|
|
||||||
property int lastPage: 0
|
|
||||||
property bool enableAnimation: false
|
|
||||||
|
|
||||||
signal requestTabChange(int newIndex)
|
|
||||||
|
|
||||||
onCurrentPageChanged: {
|
|
||||||
if (currentPage !== lastPage) {
|
|
||||||
enableAnimation = false
|
|
||||||
lastPage = currentPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && active) {
|
|
||||||
setInitialSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
loadWallpapers()
|
|
||||||
if (visible && active) {
|
|
||||||
setInitialSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onActiveChanged: {
|
|
||||||
if (active && visible) {
|
|
||||||
setInitialSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyEvent(event) {
|
|
||||||
const columns = 4
|
|
||||||
const rows = 4
|
|
||||||
const currentRow = Math.floor(gridIndex / columns)
|
|
||||||
const currentCol = gridIndex % columns
|
|
||||||
const visibleCount = wallpaperGrid.model.length
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
||||||
if (gridIndex >= 0) {
|
|
||||||
const item = wallpaperGrid.currentItem
|
|
||||||
if (item && item.wallpaperPath) {
|
|
||||||
SessionData.setWallpaper(item.wallpaperPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Right) {
|
|
||||||
if (gridIndex + 1 < visibleCount) {
|
|
||||||
// Move right within current page
|
|
||||||
gridIndex++
|
|
||||||
} else if (gridIndex === visibleCount - 1 && currentPage < totalPages - 1) {
|
|
||||||
// At last item in page, go to next page
|
|
||||||
gridIndex = 0
|
|
||||||
currentPage++
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Left) {
|
|
||||||
if (gridIndex > 0) {
|
|
||||||
// Move left within current page
|
|
||||||
gridIndex--
|
|
||||||
} else if (gridIndex === 0 && currentPage > 0) {
|
|
||||||
// At first item in page, go to previous page (last item)
|
|
||||||
currentPage--
|
|
||||||
gridIndex = Math.min(itemsPerPage - 1, wallpaperList.length - currentPage * itemsPerPage - 1)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Down) {
|
|
||||||
if (gridIndex + columns < visibleCount) {
|
|
||||||
// Move down within current page
|
|
||||||
gridIndex += columns
|
|
||||||
} else if (gridIndex >= visibleCount - columns && currentPage < totalPages - 1) {
|
|
||||||
// In last row, go to next page
|
|
||||||
gridIndex = currentCol
|
|
||||||
currentPage++
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Up) {
|
|
||||||
if (gridIndex >= columns) {
|
|
||||||
// Move up within current page
|
|
||||||
gridIndex -= columns
|
|
||||||
} else if (gridIndex < columns && currentPage > 0) {
|
|
||||||
// In first row, go to previous page (last row)
|
|
||||||
currentPage--
|
|
||||||
const prevPageCount = Math.min(itemsPerPage, wallpaperList.length - currentPage * itemsPerPage)
|
|
||||||
const prevPageRows = Math.ceil(prevPageCount / columns)
|
|
||||||
gridIndex = (prevPageRows - 1) * columns + currentCol
|
|
||||||
gridIndex = Math.min(gridIndex, prevPageCount - 1)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_PageUp && currentPage > 0) {
|
|
||||||
gridIndex = 0
|
|
||||||
currentPage--
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) {
|
|
||||||
gridIndex = 0
|
|
||||||
currentPage++
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) {
|
|
||||||
gridIndex = 0
|
|
||||||
currentPage = 0
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) {
|
|
||||||
gridIndex = 0
|
|
||||||
currentPage = totalPages - 1
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function setInitialSelection() {
|
|
||||||
if (!SessionData.wallpaperPath) {
|
|
||||||
gridIndex = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const startIndex = currentPage * itemsPerPage
|
|
||||||
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperList.length)
|
|
||||||
const pageWallpapers = wallpaperList.slice(startIndex, endIndex)
|
|
||||||
|
|
||||||
for (let i = 0; i < pageWallpapers.length; i++) {
|
|
||||||
if (pageWallpapers[i] === SessionData.wallpaperPath) {
|
|
||||||
gridIndex = i
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gridIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
onWallpaperListChanged: {
|
|
||||||
if (visible && active) {
|
|
||||||
setInitialSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadWallpapers() {
|
|
||||||
const currentWallpaper = SessionData.wallpaperPath
|
|
||||||
|
|
||||||
// Try current wallpaper path / fallback to wallpaperLastPath
|
|
||||||
if (!currentWallpaper || currentWallpaper.startsWith("#") || currentWallpaper.startsWith("we:")) {
|
|
||||||
if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") {
|
|
||||||
wallpaperDir = CacheData.wallpaperLastPath
|
|
||||||
} else {
|
|
||||||
wallpaperDir = ""
|
|
||||||
wallpaperList = []
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateWallpaperList() {
|
|
||||||
if (!wallpaperFolderModel || wallpaperFolderModel.count === 0) {
|
|
||||||
wallpaperList = []
|
|
||||||
currentPage = 0
|
|
||||||
gridIndex = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build list from FolderListModel
|
|
||||||
const files = []
|
|
||||||
for (let i = 0; i < wallpaperFolderModel.count; i++) {
|
|
||||||
const filePath = wallpaperFolderModel.get(i, "filePath")
|
|
||||||
if (filePath) {
|
|
||||||
// Remove file:// prefix if present
|
|
||||||
const cleanPath = filePath.toString().replace(/^file:\/\//, '')
|
|
||||||
files.push(cleanPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wallpaperList = files
|
|
||||||
|
|
||||||
const currentPath = SessionData.wallpaperPath
|
|
||||||
const selectedIndex = currentPath ? wallpaperList.indexOf(currentPath) : -1
|
|
||||||
|
|
||||||
if (selectedIndex >= 0) {
|
|
||||||
currentPage = Math.floor(selectedIndex / itemsPerPage)
|
|
||||||
gridIndex = selectedIndex % itemsPerPage
|
|
||||||
} else {
|
|
||||||
const maxPage = Math.max(0, Math.ceil(files.length / itemsPerPage) - 1)
|
|
||||||
currentPage = Math.min(Math.max(0, currentPage), maxPage)
|
|
||||||
gridIndex = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onWallpaperPathChanged() {
|
|
||||||
loadWallpapers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderListModel {
|
|
||||||
id: wallpaperFolderModel
|
|
||||||
|
|
||||||
showDirsFirst: false
|
|
||||||
showDotAndDotDot: false
|
|
||||||
showHidden: false
|
|
||||||
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
showFiles: true
|
|
||||||
showDirs: false
|
|
||||||
sortField: FolderListModel.Name
|
|
||||||
folder: wallpaperDir ? "file://" + wallpaperDir : ""
|
|
||||||
|
|
||||||
onStatusChanged: {
|
|
||||||
if (status === FolderListModel.Ready) {
|
|
||||||
updateWallpaperList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCountChanged: {
|
|
||||||
if (status === FolderListModel.Ready) {
|
|
||||||
updateWallpaperList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: wallpaperBrowserLoader
|
|
||||||
active: false
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: FileBrowserModal {
|
|
||||||
Component.onCompleted: {
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
browserTitle: "Select Wallpaper Directory"
|
|
||||||
browserIcon: "folder_open"
|
|
||||||
browserType: "wallpaper"
|
|
||||||
showHiddenFiles: false
|
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
allowStacking: true
|
|
||||||
|
|
||||||
onFileSelected: (path) => {
|
|
||||||
// Set the selected wallpaper
|
|
||||||
const cleanPath = path.replace(/^file:\/\//, '')
|
|
||||||
SessionData.setWallpaper(cleanPath)
|
|
||||||
|
|
||||||
// Extract directory from the selected file and load all wallpapers
|
|
||||||
const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'))
|
|
||||||
if (dirPath) {
|
|
||||||
wallpaperDir = dirPath
|
|
||||||
CacheData.wallpaperLastPath = dirPath
|
|
||||||
CacheData.saveCache()
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
onDialogClosed: {
|
|
||||||
Qt.callLater(() => wallpaperBrowserLoader.active = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 50
|
|
||||||
|
|
||||||
GridView {
|
|
||||||
id: wallpaperGrid
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingS
|
|
||||||
height: parent.height - Theme.spacingS
|
|
||||||
cellWidth: width / 4
|
|
||||||
cellHeight: height / 4
|
|
||||||
clip: true
|
|
||||||
enabled: root.active
|
|
||||||
interactive: root.active
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
keyNavigationEnabled: false
|
|
||||||
activeFocusOnTab: false
|
|
||||||
highlightFollowsCurrentItem: true
|
|
||||||
highlightMoveDuration: enableAnimation ? Theme.shortDuration : 0
|
|
||||||
focus: false
|
|
||||||
|
|
||||||
highlight: Item {
|
|
||||||
z: 1000
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingXS
|
|
||||||
color: "transparent"
|
|
||||||
border.width: 3
|
|
||||||
border.color: Theme.primary
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
model: {
|
|
||||||
const startIndex = currentPage * itemsPerPage
|
|
||||||
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperList.length)
|
|
||||||
return wallpaperList.slice(startIndex, endIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
onModelChanged: {
|
|
||||||
const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0
|
|
||||||
if (gridIndex !== clampedIndex) {
|
|
||||||
gridIndex = clampedIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCountChanged: {
|
|
||||||
if (count > 0) {
|
|
||||||
const clampedIndex = Math.min(gridIndex, count - 1)
|
|
||||||
currentIndex = clampedIndex
|
|
||||||
positionViewAtIndex(clampedIndex, GridView.Contain)
|
|
||||||
}
|
|
||||||
enableAnimation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onGridIndexChanged() {
|
|
||||||
if (enableAnimation && wallpaperGrid.count > 0) {
|
|
||||||
wallpaperGrid.currentIndex = gridIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
width: wallpaperGrid.cellWidth
|
|
||||||
height: wallpaperGrid.cellHeight
|
|
||||||
|
|
||||||
property string wallpaperPath: modelData || ""
|
|
||||||
property bool isSelected: SessionData.wallpaperPath === modelData
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: wallpaperCard
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingXS
|
|
||||||
color: Theme.surfaceContainerHighest
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
|
|
||||||
radius: parent.radius
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: thumbnailImage
|
|
||||||
anchors.fill: parent
|
|
||||||
source: modelData ? `file://${modelData}` : ""
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
asynchronous: true
|
|
||||||
cache: true
|
|
||||||
smooth: true
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1.0
|
|
||||||
maskSource: ShaderEffectSource {
|
|
||||||
sourceItem: Rectangle {
|
|
||||||
width: thumbnailImage.width
|
|
||||||
height: thumbnailImage.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BusyIndicator {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
running: thumbnailImage.status === Image.Loading
|
|
||||||
visible: running
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
anchors.fill: parent
|
|
||||||
cornerRadius: parent.radius
|
|
||||||
stateColor: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: wallpaperMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
gridIndex = index
|
|
||||||
if (modelData) {
|
|
||||||
SessionData.setWallpaper(modelData)
|
|
||||||
}
|
|
||||||
// Don't steal focus - let mainContainer keep it for keyboard nav
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: wallpaperList.length === 0
|
|
||||||
text: "No wallpapers found\n\nClick the folder icon below to browse"
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: Theme.outline
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: (parent.width - controlsRow.width - browseButton.width - Theme.spacingS) / 2
|
|
||||||
height: parent.height
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: controlsRow
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
iconName: "skip_previous"
|
|
||||||
iconSize: 20
|
|
||||||
buttonSize: 32
|
|
||||||
enabled: currentPage > 0
|
|
||||||
opacity: enabled ? 1.0 : 0.3
|
|
||||||
onClicked: {
|
|
||||||
if (currentPage > 0) {
|
|
||||||
currentPage--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: wallpaperList.length > 0 ? `${wallpaperList.length} wallpapers • ${currentPage + 1} / ${totalPages}` : "No wallpapers"
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
iconName: "skip_next"
|
|
||||||
iconSize: 20
|
|
||||||
buttonSize: 32
|
|
||||||
enabled: currentPage < totalPages - 1
|
|
||||||
opacity: enabled ? 1.0 : 0.3
|
|
||||||
onClicked: {
|
|
||||||
if (currentPage < totalPages - 1) {
|
|
||||||
currentPage++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: browseButton
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
iconName: "folder_open"
|
|
||||||
iconSize: 20
|
|
||||||
buttonSize: 32
|
|
||||||
opacity: 0.7
|
|
||||||
onClicked: wallpaperBrowserLoader.active = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,642 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
implicitWidth: 700
|
|
||||||
implicitHeight: 410
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "cloud_off"
|
|
||||||
size: Theme.iconSize * 2
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("No Weather Data Available")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 70
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: refreshButton
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
|
|
||||||
property bool isRefreshing: false
|
|
||||||
enabled: !isRefreshing
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
|
||||||
onClicked: {
|
|
||||||
refreshButton.isRefreshing = true
|
|
||||||
WeatherService.forceRefresh()
|
|
||||||
refreshTimer.restart()
|
|
||||||
}
|
|
||||||
enabled: parent.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: refreshTimer
|
|
||||||
interval: 2000
|
|
||||||
onTriggered: refreshButton.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation on rotation {
|
|
||||||
running: refreshButton.isRefreshing
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2
|
|
||||||
height: 70
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: weatherIcon
|
|
||||||
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
|
||||||
size: Theme.iconSize * 1.5
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 4
|
|
||||||
shadowBlur: 0.8
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.2)
|
|
||||||
shadowOpacity: 0.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: tempColumn
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.left: weatherIcon.right
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: tempText.width + unitText.width + Theme.spacingXS
|
|
||||||
height: tempText.height
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tempText
|
|
||||||
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge + 4
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Light
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: unitText
|
|
||||||
text: SettingsData.useFahrenheit ? "F" : "C"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.left: tempText.right
|
|
||||||
anchors.leftMargin: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (WeatherService.weather.available) {
|
|
||||||
SettingsData.setTemperatureUnit(!SettingsData.useFahrenheit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enabled: WeatherService.weather.available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.city || ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: sunriseColumn
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.left: tempColumn.right
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: WeatherService.weather.sunrise && WeatherService.weather.sunset
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: sunriseIcon.width + sunriseText.width + Theme.spacingXS
|
|
||||||
height: sunriseIcon.height
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: sunriseIcon
|
|
||||||
name: "wb_twilight"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: sunriseText
|
|
||||||
text: WeatherService.weather.sunrise || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
anchors.left: sunriseIcon.right
|
|
||||||
anchors.leftMargin: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: sunsetIcon.width + sunsetText.width + Theme.spacingXS
|
|
||||||
height: sunsetIcon.height
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: sunsetIcon
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: sunsetText
|
|
||||||
text: WeatherService.weather.sunset || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
anchors.left: sunsetIcon.right
|
|
||||||
anchors.leftMargin: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
GridLayout {
|
|
||||||
width: parent.width
|
|
||||||
height: 95
|
|
||||||
columns: 6
|
|
||||||
columnSpacing: Theme.spacingS
|
|
||||||
rowSpacing: 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "device_thermostat"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Feels Like")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall + 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "humidity_low"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Humidity")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall + 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "air"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Wind")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.wind || "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall + 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "speed"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Pressure")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.pressure ? WeatherService.weather.pressure + " hPa" : "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall + 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "rainy"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Rain Chance")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall + 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "wb_sunny"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Visibility")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Good")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall + 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 70 - 95 - Theme.spacingM * 3 - 2
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("7-Day Forecast")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - Theme.fontSizeMedium - Theme.spacingS - Theme.spacingL
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: 7
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingXS * 6) / 7
|
|
||||||
height: parent.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
property var dayDate: {
|
|
||||||
const date = new Date()
|
|
||||||
date.setDate(date.getDate() + index)
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
property bool isToday: index === 0
|
|
||||||
property var forecastData: {
|
|
||||||
if (WeatherService.weather.forecast && WeatherService.weather.forecast.length > index) {
|
|
||||||
return WeatherService.weather.forecast[index]
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : Theme.surfaceContainerHigh
|
|
||||||
border.color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
|
|
||||||
border.width: isToday ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: Qt.locale().dayName(dayDate.getDay(), Locale.ShortFormat)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: isToday ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: isToday ? Font.Medium : Font.Normal
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: forecastData ? WeatherService.getWeatherIcon(forecastData.wCode || 0) : "cloud"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: isToday ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: forecastData ? (SettingsData.useFahrenheit ? (forecastData.tempMaxF || forecastData.tempMax) : (forecastData.tempMax || 0)) + "°/" + (SettingsData.useFahrenheit ? (forecastData.tempMinF || forecastData.tempMin) : (forecastData.tempMin || 0)) + "°" : "--/--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: isToday ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 1
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: forecastData && forecastData.sunrise && forecastData.sunset
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "wb_twilight"
|
|
||||||
size: 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: forecastData ? forecastData.sunrise : ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall - 2
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: forecastData ? forecastData.sunset : ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall - 2
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
id: dockVariants
|
|
||||||
model: SettingsData.getFilteredScreens("dock")
|
|
||||||
|
|
||||||
property var contextMenu
|
|
||||||
|
|
||||||
delegate: PanelWindow {
|
|
||||||
id: dock
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "quickshell:dock"
|
|
||||||
|
|
||||||
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
|
|
||||||
bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
|
|
||||||
left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
|
|
||||||
right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
|
|
||||||
}
|
|
||||||
|
|
||||||
property var modelData: item
|
|
||||||
property bool autoHide: SettingsData.dockAutoHide
|
|
||||||
property real backgroundTransparency: SettingsData.dockTransparency
|
|
||||||
property bool groupByApp: SettingsData.dockGroupByApp
|
|
||||||
|
|
||||||
readonly property real widgetHeight: SettingsData.dockIconSize
|
|
||||||
readonly property real effectiveBarHeight: widgetHeight + SettingsData.dockSpacing * 2 + 10
|
|
||||||
readonly property real barSpacing: {
|
|
||||||
const barIsHorizontal = (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)
|
|
||||||
const barIsVertical = (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)
|
|
||||||
const samePosition = (SettingsData.dockPosition === SettingsData.dankBarPosition)
|
|
||||||
const dockIsHorizontal = !isVertical
|
|
||||||
const dockIsVertical = isVertical
|
|
||||||
|
|
||||||
if (!SettingsData.dankBarVisible) return 0
|
|
||||||
if (dockIsHorizontal && barIsHorizontal && samePosition) {
|
|
||||||
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
|
|
||||||
}
|
|
||||||
if (dockIsVertical && barIsVertical && samePosition) {
|
|
||||||
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real dockMargin: SettingsData.dockSpacing
|
|
||||||
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap
|
|
||||||
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
|
|
||||||
function px(v) { return Math.round(v * _dpr) / _dpr }
|
|
||||||
|
|
||||||
|
|
||||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
|
|
||||||
property bool revealSticky: false
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: revealHold
|
|
||||||
interval: 250
|
|
||||||
repeat: false
|
|
||||||
onTriggered: dock.revealSticky = false
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool reveal: {
|
|
||||||
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
|
|
||||||
}
|
|
||||||
|
|
||||||
onContextMenuOpenChanged: {
|
|
||||||
if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) {
|
|
||||||
revealSticky = true
|
|
||||||
revealHold.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onDockTransparencyChanged() {
|
|
||||||
dock.backgroundTransparency = SettingsData.dockTransparency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: modelData
|
|
||||||
visible: {
|
|
||||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
|
||||||
return SettingsData.dockOpenOnOverview
|
|
||||||
}
|
|
||||||
return SettingsData.showDock
|
|
||||||
}
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
|
|
||||||
exclusiveZone: {
|
|
||||||
if (!SettingsData.showDock || autoHide) return -1
|
|
||||||
if (barSpacing > 0) return -1
|
|
||||||
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap)
|
|
||||||
}
|
|
||||||
|
|
||||||
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
|
|
||||||
|
|
||||||
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
|
||||||
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: maskItem
|
|
||||||
parent: dock.contentItem
|
|
||||||
visible: false
|
|
||||||
x: {
|
|
||||||
const baseX = dockCore.x + dockMouseArea.x
|
|
||||||
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) {
|
|
||||||
return baseX - animationHeadroom
|
|
||||||
}
|
|
||||||
return baseX
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
const baseY = dockCore.y + dockMouseArea.y
|
|
||||||
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
|
||||||
return baseY - animationHeadroom
|
|
||||||
}
|
|
||||||
return baseY
|
|
||||||
}
|
|
||||||
width: dockMouseArea.width + (isVertical ? animationHeadroom : 0)
|
|
||||||
height: dockMouseArea.height + (!isVertical ? animationHeadroom : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
mask: Region {
|
|
||||||
item: maskItem
|
|
||||||
}
|
|
||||||
|
|
||||||
property var hoveredButton: {
|
|
||||||
if (!dockApps.children[0]) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const layoutItem = dockApps.children[0]
|
|
||||||
const flowLayout = layoutItem.children[0]
|
|
||||||
let repeater = null
|
|
||||||
for (var i = 0; i < flowLayout.children.length; i++) {
|
|
||||||
const child = flowLayout.children[i]
|
|
||||||
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
|
|
||||||
repeater = child
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!repeater || !repeater.itemAt) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
for (var i = 0; i < repeater.count; i++) {
|
|
||||||
const item = repeater.itemAt(i)
|
|
||||||
if (item && item.dockButton && item.dockButton.showTooltip) {
|
|
||||||
return item.dockButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTooltip {
|
|
||||||
id: dockTooltip
|
|
||||||
targetScreen: dock.screen
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: tooltipRevealDelay
|
|
||||||
interval: 250
|
|
||||||
repeat: false
|
|
||||||
onTriggered: dock.showTooltipForHoveredButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
function showTooltipForHoveredButton() {
|
|
||||||
dockTooltip.hide()
|
|
||||||
if (dock.hoveredButton && dock.reveal && !slideXAnimation.running && !slideYAnimation.running) {
|
|
||||||
const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0)
|
|
||||||
const tooltipText = dock.hoveredButton.tooltipText || ""
|
|
||||||
if (tooltipText) {
|
|
||||||
const screenX = dock.screen ? (dock.screen.x || 0) : 0
|
|
||||||
const screenY = dock.screen ? (dock.screen.y || 0) : 0
|
|
||||||
const screenHeight = dock.screen ? dock.screen.height : 0
|
|
||||||
if (!dock.isVertical) {
|
|
||||||
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
|
||||||
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2
|
|
||||||
const screenRelativeY = isBottom
|
|
||||||
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - 35)
|
|
||||||
: (buttonGlobalPos.y - screenY + dock.hoveredButton.height + Theme.spacingS)
|
|
||||||
dockTooltip.show(tooltipText,
|
|
||||||
globalX,
|
|
||||||
screenRelativeY,
|
|
||||||
dock.screen,
|
|
||||||
false, false)
|
|
||||||
} else {
|
|
||||||
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left
|
|
||||||
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + Theme.spacingXS
|
|
||||||
const tooltipX = isLeft ? tooltipOffset : (dock.screen.width - tooltipOffset)
|
|
||||||
const screenRelativeY = buttonGlobalPos.y - screenY + dock.hoveredButton.height / 2
|
|
||||||
dockTooltip.show(tooltipText,
|
|
||||||
screenX + tooltipX,
|
|
||||||
screenRelativeY,
|
|
||||||
dock.screen,
|
|
||||||
isLeft,
|
|
||||||
!isLeft)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: dock
|
|
||||||
function onRevealChanged() {
|
|
||||||
if (!dock.reveal) {
|
|
||||||
tooltipRevealDelay.stop()
|
|
||||||
dockTooltip.hide()
|
|
||||||
} else {
|
|
||||||
tooltipRevealDelay.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onHoveredButtonChanged() {
|
|
||||||
dock.showTooltipForHoveredButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: dockCore
|
|
||||||
anchors.fill: parent
|
|
||||||
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
|
|
||||||
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: dockMouseArea
|
|
||||||
function onContainsMouseChanged() {
|
|
||||||
if (dockMouseArea.containsMouse) {
|
|
||||||
dock.revealSticky = true
|
|
||||||
revealHold.stop()
|
|
||||||
} else {
|
|
||||||
if (dock.autoHide && !dock.contextMenuOpen) {
|
|
||||||
revealHold.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dockMouseArea
|
|
||||||
property real currentScreen: modelData ? modelData : dock.screen
|
|
||||||
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
|
|
||||||
property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080
|
|
||||||
property real maxDockWidth: screenWidth * 0.98
|
|
||||||
property real maxDockHeight: screenHeight * 0.98
|
|
||||||
|
|
||||||
height: {
|
|
||||||
if (dock.isVertical) {
|
|
||||||
return dock.reveal ? Math.min(dockBackground.implicitHeight + 4, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
|
|
||||||
} else {
|
|
||||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width: {
|
|
||||||
if (dock.isVertical) {
|
|
||||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
|
||||||
} else {
|
|
||||||
return dock.reveal ? Math.min(dockBackground.implicitWidth + 4, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
anchors {
|
|
||||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
|
|
||||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
|
||||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
|
||||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
|
|
||||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
|
||||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
|
||||||
}
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: dockContainer
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
id: dockSlide
|
|
||||||
x: {
|
|
||||||
if (!dock.isVertical) return 0
|
|
||||||
if (dock.reveal) return 0
|
|
||||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
|
|
||||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
|
||||||
return hideDistance
|
|
||||||
} else {
|
|
||||||
return -hideDistance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (dock.isVertical) return 0
|
|
||||||
if (dock.reveal) return 0
|
|
||||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
|
|
||||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
|
||||||
return hideDistance
|
|
||||||
} else {
|
|
||||||
return -hideDistance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
id: slideXAnimation
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
id: slideYAnimation
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: dockBackground
|
|
||||||
objectName: "dockBackground"
|
|
||||||
anchors {
|
|
||||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
|
|
||||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
|
||||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
|
||||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
|
|
||||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
|
||||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
|
||||||
}
|
|
||||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + 1 : 0
|
|
||||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + 1 : 0
|
|
||||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + 1 : 0
|
|
||||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + 1 : 0
|
|
||||||
|
|
||||||
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
|
|
||||||
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
|
|
||||||
width: implicitWidth
|
|
||||||
height: implicitHeight
|
|
||||||
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
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: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
|
|
||||||
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
|
|
||||||
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
|
|
||||||
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
|
|
||||||
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
|
|
||||||
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
|
|
||||||
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
|
||||||
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
|
||||||
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
|
||||||
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
|
||||||
|
|
||||||
contextMenu: dockVariants.contextMenu
|
|
||||||
groupByApp: dock.groupByApp
|
|
||||||
isVertical: dock.isVertical
|
|
||||||
dockScreen: dock.screen
|
|
||||||
iconSize: dock.widgetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,564 +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 var parentDockScreen: null
|
|
||||||
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 var cachedDesktopEntry: null
|
|
||||||
property real actualIconSize: 40
|
|
||||||
|
|
||||||
function updateDesktopEntry() {
|
|
||||||
if (!appData || appData.appId === "__SEPARATOR__") {
|
|
||||||
cachedDesktopEntry = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const moddedId = Paths.moddedAppId(appData.appId)
|
|
||||||
cachedDesktopEntry = DesktopEntries.heuristicLookup(moddedId)
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: updateDesktopEntry()
|
|
||||||
|
|
||||||
onAppDataChanged: updateDesktopEntry()
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DesktopEntries
|
|
||||||
function onApplicationsChanged() {
|
|
||||||
updateDesktopEntry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 appName = cachedDesktopEntry && cachedDesktopEntry.name ? cachedDesktopEntry.name : appData.appId
|
|
||||||
const title = appData.type === "window" ? windowTitle : appData.windowTitle
|
|
||||||
return appName + (title ? " • " + title : "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!appData.appId) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedDesktopEntry && cachedDesktopEntry.name ? cachedDesktopEntry.name : appData.appId
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (mouseArea.pressed) return
|
|
||||||
|
|
||||||
if (isHovered) {
|
|
||||||
exitAnimation.stop()
|
|
||||||
if (!bounceAnimation.running) {
|
|
||||||
bounceAnimation.restart()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bounceAnimation.stop()
|
|
||||||
exitAnimation.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property bool animateX: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
|
||||||
readonly property real animationDistance: actualIconSize
|
|
||||||
readonly property real animationDirection: {
|
|
||||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) return -1
|
|
||||||
if (SettingsData.dockPosition === SettingsData.Position.Top) return 1
|
|
||||||
if (SettingsData.dockPosition === SettingsData.Position.Right) return -1
|
|
||||||
if (SettingsData.dockPosition === SettingsData.Position.Left) return 1
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: bounceAnimation
|
|
||||||
|
|
||||||
running: false
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: iconTransform
|
|
||||||
property: animateX ? "x" : "y"
|
|
||||||
to: animationDirection * animationDistance * 0.25
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedAccel
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: iconTransform
|
|
||||||
property: animateX ? "x" : "y"
|
|
||||||
to: animationDirection * animationDistance * 0.2
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedDecel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: exitAnimation
|
|
||||||
|
|
||||||
running: false
|
|
||||||
target: iconTransform
|
|
||||||
property: animateX ? "x" : "y"
|
|
||||||
to: 0
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedDecel
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: longPressTimer
|
|
||||||
|
|
||||||
interval: 500
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (appData && appData.isPinned) {
|
|
||||||
longPressing = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
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 = actualIconSize
|
|
||||||
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 = cachedDesktopEntry
|
|
||||||
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 = cachedDesktopEntry
|
|
||||||
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 {
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.showForButton(root, appData, root.height + 25, true, cachedDesktopEntry, parentDockScreen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.MiddleButton) {
|
|
||||||
if (appData && appData.type === "window") {
|
|
||||||
const sortedToplevels = CompositorService.sortedToplevels
|
|
||||||
for (var i = 0; i < sortedToplevels.length; i++) {
|
|
||||||
const toplevel = sortedToplevels[i]
|
|
||||||
const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
|
|
||||||
if (checkId === appData.uniqueId) {
|
|
||||||
toplevel.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (appData && appData.type === "grouped") {
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
|
|
||||||
}
|
|
||||||
} else if (appData && appData.appId) {
|
|
||||||
const desktopEntry = cachedDesktopEntry
|
|
||||||
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) {
|
|
||||||
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
|
|
||||||
} else {
|
|
||||||
console.warn("No context menu or appData available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: visualContent
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
id: iconTransform
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
implicitSize: actualIconSize
|
|
||||||
source: {
|
|
||||||
if (appData.appId === "__SEPARATOR__") {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
const moddedId = Paths.moddedAppId(appData.appId)
|
|
||||||
if (moddedId.toLowerCase().includes("steam_app")) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return cachedDesktopEntry && cachedDesktopEntry.icon ? Quickshell.iconPath(cachedDesktopEntry.icon, true) : ""
|
|
||||||
}
|
|
||||||
mipmap: true
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
size: actualIconSize
|
|
||||||
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: actualIconSize
|
|
||||||
height: actualIconSize
|
|
||||||
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 = cachedDesktopEntry
|
|
||||||
if (desktopEntry && desktopEntry.name) {
|
|
||||||
return desktopEntry.name.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
return appData.appId.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
font.pixelSize: Math.max(8, parent.width * 0.35)
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.horizontalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.horizontalCenter
|
|
||||||
anchors.verticalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? parent.verticalCenter : undefined
|
|
||||||
anchors.bottom: SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined
|
|
||||||
anchors.top: SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined
|
|
||||||
anchors.left: SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
|
|
||||||
anchors.right: SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
|
|
||||||
anchors.bottomMargin: SettingsData.dockPosition === SettingsData.Position.Bottom ? -2 : 0
|
|
||||||
anchors.topMargin: SettingsData.dockPosition === SettingsData.Position.Top ? -2 : 0
|
|
||||||
anchors.leftMargin: SettingsData.dockPosition === SettingsData.Position.Left ? -2 : 0
|
|
||||||
anchors.rightMargin: SettingsData.dockPosition === SettingsData.Position.Right ? -2 : 0
|
|
||||||
|
|
||||||
sourceComponent: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? columnIndicator : rowIndicator
|
|
||||||
|
|
||||||
visible: {
|
|
||||||
if (!appData) return false
|
|
||||||
if (appData.type === "window") return true
|
|
||||||
if (appData.type === "grouped") return appData.windowCount > 0
|
|
||||||
return appData.isRunning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: rowIndicator
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
if (!appData) return 0
|
|
||||||
if (appData.type === "grouped") {
|
|
||||||
return Math.min(appData.windowCount, 4)
|
|
||||||
} else if (appData.type === "window" || appData.isRunning) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2)
|
|
||||||
height: Math.max(2, actualIconSize * 0.05)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (!appData) {
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.type !== "grouped" || appData.windowCount === 1) {
|
|
||||||
if (isWindowFocused) {
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.type === "grouped" && appData.windowCount > 1) {
|
|
||||||
const groupToplevels = getGroupedToplevels()
|
|
||||||
if (index < groupToplevels.length && groupToplevels[index].activated) {
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: columnIndicator
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
if (!appData) return 0
|
|
||||||
if (appData.type === "grouped") {
|
|
||||||
return Math.min(appData.windowCount, 4)
|
|
||||||
} else if (appData.type === "window" || appData.isRunning) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(2, actualIconSize * 0.05)
|
|
||||||
height: appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (!appData) {
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.type !== "grouped" || appData.windowCount === 1) {
|
|
||||||
if (isWindowFocused) {
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.type === "grouped" && appData.windowCount > 1) {
|
|
||||||
const groupToplevels = getGroupedToplevels()
|
|
||||||
if (index < groupToplevels.length && groupToplevels[index].activated) {
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,263 +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
|
|
||||||
property bool isVertical: false
|
|
||||||
property var dockScreen: null
|
|
||||||
property real iconSize: 40
|
|
||||||
|
|
||||||
clip: false
|
|
||||||
implicitWidth: isVertical ? appLayout.height : appLayout.width
|
|
||||||
implicitHeight: isVertical ? appLayout.width : appLayout.height
|
|
||||||
|
|
||||||
function movePinnedApp(fromIndex, toIndex) {
|
|
||||||
if (fromIndex === toIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPinned = [...(SessionData.pinnedApps || [])]
|
|
||||||
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedApp = currentPinned.splice(fromIndex, 1)[0]
|
|
||||||
currentPinned.splice(toIndex, 0, movedApp)
|
|
||||||
|
|
||||||
SessionData.setPinnedApps(currentPinned)
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: appLayout
|
|
||||||
width: layoutFlow.width
|
|
||||||
height: layoutFlow.height
|
|
||||||
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
|
|
||||||
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
|
|
||||||
anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
|
|
||||||
anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
|
|
||||||
anchors.top: root.isVertical ? undefined : parent.top
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
id: layoutFlow
|
|
||||||
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
|
||||||
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: repeater
|
|
||||||
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
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
width: model.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
|
|
||||||
height: model.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: model.type === "separator"
|
|
||||||
width: root.isVertical ? root.iconSize * 0.5 : 2
|
|
||||||
height: root.isVertical ? 2 : root.iconSize * 0.5
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
|
||||||
radius: 1
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
visible: model.type === "separator"
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
}
|
|
||||||
|
|
||||||
DockAppButton {
|
|
||||||
id: button
|
|
||||||
visible: model.type !== "separator"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
width: delegateItem.width
|
|
||||||
height: delegateItem.height
|
|
||||||
actualIconSize: root.iconSize
|
|
||||||
|
|
||||||
appData: model
|
|
||||||
contextMenu: root.contextMenu
|
|
||||||
dockApps: root
|
|
||||||
index: model.index
|
|
||||||
parentDockScreen: root.dockScreen
|
|
||||||
|
|
||||||
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,25 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
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,459 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isVisible: false
|
|
||||||
property bool showLogout: true
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int optionCount: {
|
|
||||||
let count = 0
|
|
||||||
if (showLogout) count++
|
|
||||||
count++
|
|
||||||
if (SessionService.hibernateSupported) count++
|
|
||||||
count += 2
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
signal closed()
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
isVisible = true
|
|
||||||
selectedIndex = 0
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (powerMenuFocusScope && powerMenuFocusScope.forceActiveFocus) {
|
|
||||||
powerMenuFocusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
isVisible = false
|
|
||||||
closed()
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.5)
|
|
||||||
visible: isVisible
|
|
||||||
z: 1000
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: powerMenuFocusScope
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: root.isVisible
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
Qt.callLater(() => forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onEscapePressed: {
|
|
||||||
root.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Up:
|
|
||||||
case Qt.Key_Backtab:
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Down:
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
const actions = []
|
|
||||||
if (showLogout) actions.push("logout")
|
|
||||||
actions.push("suspend")
|
|
||||||
if (SessionService.hibernateSupported) actions.push("hibernate")
|
|
||||||
actions.push("reboot", "poweroff")
|
|
||||||
if (selectedIndex < actions.length) {
|
|
||||||
const action = actions[selectedIndex]
|
|
||||||
hide()
|
|
||||||
switch (action) {
|
|
||||||
case "logout":
|
|
||||||
SessionService.logout()
|
|
||||||
break
|
|
||||||
case "suspend":
|
|
||||||
SessionService.suspend()
|
|
||||||
break
|
|
||||||
case "hibernate":
|
|
||||||
SessionService.hibernate()
|
|
||||||
break
|
|
||||||
case "reboot":
|
|
||||||
SessionService.reboot()
|
|
||||||
break
|
|
||||||
case "poweroff":
|
|
||||||
SessionService.poweroff()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_N:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_P:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_J:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_K:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 320
|
|
||||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
height: implicitHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Options")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
visible: showLogout
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 0) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (logoutArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === 0 ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Log Out")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
const suspendIdx = showLogout ? 1 : 0
|
|
||||||
if (selectedIndex === suspendIdx) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (suspendArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === (showLogout ? 1 : 0) ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === (showLogout ? 1 : 0) ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Suspend")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.suspend()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
const hibernateIdx = showLogout ? 2 : 1
|
|
||||||
if (selectedIndex === hibernateIdx) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (hibernateArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === (showLogout ? 2 : 1) ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === (showLogout ? 2 : 1) ? 1 : 0
|
|
||||||
visible: SessionService.hibernateSupported
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "ac_unit"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Hibernate")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: hibernateArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.hibernate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
let rebootIdx = showLogout ? 3 : 2
|
|
||||||
if (!SessionService.hibernateSupported) rebootIdx--
|
|
||||||
if (selectedIndex === rebootIdx) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (rebootArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
let rebootIdx = showLogout ? 3 : 2
|
|
||||||
if (!SessionService.hibernateSupported) rebootIdx--
|
|
||||||
return selectedIndex === rebootIdx ? Theme.primary : "transparent"
|
|
||||||
}
|
|
||||||
border.width: {
|
|
||||||
let rebootIdx = showLogout ? 3 : 2
|
|
||||||
if (!SessionService.hibernateSupported) rebootIdx--
|
|
||||||
return selectedIndex === rebootIdx ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "restart_alt"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Reboot")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.reboot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
let powerOffIdx = showLogout ? 4 : 3
|
|
||||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
|
||||||
if (selectedIndex === powerOffIdx) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (powerOffArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
let powerOffIdx = showLogout ? 4 : 3
|
|
||||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
|
||||||
return selectedIndex === powerOffIdx ? Theme.primary : "transparent"
|
|
||||||
}
|
|
||||||
border.width: {
|
|
||||||
let powerOffIdx = showLogout ? 4 : 3
|
|
||||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
|
||||||
return selectedIndex === powerOffIdx ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power_settings_new"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Off")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: powerOffArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.poweroff()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
height: Theme.spacingS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankOSD {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
|
||||||
osdHeight: 40 + Theme.spacingS * 2
|
|
||||||
autoHideInterval: 3000
|
|
||||||
enableMouseInteraction: true
|
|
||||||
|
|
||||||
property var brightnessDebounceTimer: Timer {
|
|
||||||
property int pendingValue: 0
|
|
||||||
|
|
||||||
interval: {
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
|
||||||
return (deviceInfo && deviceInfo.class === "ddc") ? 200 : 50
|
|
||||||
}
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
DisplayService.setBrightnessInternal(pendingValue, DisplayService.lastIpcDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DisplayService
|
|
||||||
function onBrightnessChanged() {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int gap: Theme.spacingS
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Theme.iconSize
|
|
||||||
height: Theme.iconSize
|
|
||||||
radius: Theme.iconSize / 2
|
|
||||||
color: "transparent"
|
|
||||||
x: parent.gap
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
|
||||||
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
|
|
||||||
return "brightness_medium"
|
|
||||||
} else if (deviceInfo.name.includes("kbd")) {
|
|
||||||
return "keyboard"
|
|
||||||
} else {
|
|
||||||
return "lightbulb"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: brightnessSlider
|
|
||||||
|
|
||||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
|
||||||
height: 40
|
|
||||||
x: parent.gap * 2 + Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
minimum: 1
|
|
||||||
maximum: 100
|
|
||||||
enabled: DisplayService.brightnessAvailable
|
|
||||||
showValue: true
|
|
||||||
unit: "%"
|
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
|
||||||
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
root.brightnessDebounceTimer.pendingValue = newValue
|
|
||||||
root.brightnessDebounceTimer.restart()
|
|
||||||
resetHideTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onContainsMouseChanged: {
|
|
||||||
setChildHovered(containsMouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderDragFinished: finalValue => {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
root.brightnessDebounceTimer.stop()
|
|
||||||
DisplayService.setBrightnessInternal(finalValue, DisplayService.lastIpcDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DisplayService
|
|
||||||
|
|
||||||
function onBrightnessChanged() {
|
|
||||||
if (!brightnessSlider.pressed) {
|
|
||||||
brightnessSlider.value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeviceSwitched() {
|
|
||||||
if (!brightnessSlider.pressed) {
|
|
||||||
brightnessSlider.value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOsdShown: {
|
|
||||||
if (DisplayService.brightnessAvailable && contentLoader.item) {
|
|
||||||
const slider = contentLoader.item.children[0].children[1]
|
|
||||||
if (slider) {
|
|
||||||
slider.value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankOSD {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
|
||||||
osdHeight: 40 + Theme.spacingS * 2
|
|
||||||
autoHideInterval: 3000
|
|
||||||
enableMouseInteraction: true
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
|
||||||
|
|
||||||
function onVolumeChanged() {
|
|
||||||
if (!AudioService.suppressOSD) {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMutedChanged() {
|
|
||||||
if (!AudioService.suppressOSD) {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService
|
|
||||||
|
|
||||||
function onSinkChanged() {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int gap: Theme.spacingS
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Theme.iconSize
|
|
||||||
height: Theme.iconSize
|
|
||||||
radius: Theme.iconSize / 2
|
|
||||||
color: "transparent"
|
|
||||||
x: parent.gap
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: muteButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
AudioService.toggleMute()
|
|
||||||
}
|
|
||||||
onContainsMouseChanged: {
|
|
||||||
setChildHovered(containsMouse || volumeSlider.containsMouse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: volumeSlider
|
|
||||||
|
|
||||||
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
|
||||||
readonly property real displayPercent: actualVolumePercent
|
|
||||||
|
|
||||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
|
||||||
height: 40
|
|
||||||
x: parent.gap * 2 + Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
enabled: AudioService.sink && AudioService.sink.audio
|
|
||||||
showValue: true
|
|
||||||
unit: "%"
|
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
|
||||||
valueOverride: displayPercent
|
|
||||||
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.suppressOSD = true
|
|
||||||
AudioService.sink.audio.volume = newValue / 100
|
|
||||||
AudioService.suppressOSD = false
|
|
||||||
resetHideTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onContainsMouseChanged: {
|
|
||||||
setChildHovered(containsMouse || muteButton.containsMouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
|
||||||
|
|
||||||
function onVolumeChanged() {
|
|
||||||
if (volumeSlider && !volumeSlider.pressed) {
|
|
||||||
volumeSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOsdShown: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio && contentLoader.item) {
|
|
||||||
const slider = contentLoader.item.children[0].children[1]
|
|
||||||
if (slider) {
|
|
||||||
slider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var axis: null
|
|
||||||
property string section: "center"
|
|
||||||
property var popoutTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
property alias content: contentLoader.sourceComponent
|
|
||||||
property bool isVerticalOrientation: axis?.isVertical ?? false
|
|
||||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
|
|
||||||
readonly property real visualWidth: isVerticalOrientation ? widgetThickness : (contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0)
|
|
||||||
readonly property real visualHeight: isVerticalOrientation ? (contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0) : widgetThickness
|
|
||||||
readonly property alias visualContent: visualContent
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
width: isVerticalOrientation ? barThickness : visualWidth
|
|
||||||
height: isVerticalOrientation ? visualHeight : barThickness
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: root.visualWidth
|
|
||||||
height: root.visualHeight
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.dankBarNoBackground) {
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
z: -1
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.clicked()
|
|
||||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
|
||||||
const globalPos = mapToGlobal(0, 0)
|
|
||||||
const currentScreen = parentScreen || Screen
|
|
||||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
|
||||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,560 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: aboutTab
|
|
||||||
|
|
||||||
property bool isHyprland: CompositorService.isHyprland
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// ASCII Art Header
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: asciiSection.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: asciiSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: asciiText.implicitHeight
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: asciiText
|
|
||||||
|
|
||||||
text: "██████╗ █████╗ ███╗ ██╗██╗ ██╗\n██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝\n██║ ██║███████║██╔██╗ ██║█████╔╝ \n██║ ██║██╔══██║██║╚██╗██║██╔═██╗ \n██████╔╝██║ ██║██║ ╚████║██║ ██╗\n╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝"
|
|
||||||
isMonospace: true
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: SystemUpdateService.shellVersion ? `dms ${SystemUpdateService.shellVersion}` : "dms"
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: communityIcons
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
height: 24
|
|
||||||
width: {
|
|
||||||
if (isHyprland) {
|
|
||||||
return compositorButton.width + discordButton.width + Theme.spacingM + redditButton.width + Theme.spacingM
|
|
||||||
} else {
|
|
||||||
return compositorButton.width + matrixButton.width + 4 + discordButton.width + Theme.spacingM + redditButton.width + Theme.spacingM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compositor logo (Niri or Hyprland)
|
|
||||||
Item {
|
|
||||||
id: compositorButton
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.verticalCenterOffset: -2
|
|
||||||
x: 0
|
|
||||||
|
|
||||||
property bool hovered: false
|
|
||||||
property string tooltipText: isHyprland ? "Hyprland Website" : "niri GitHub"
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Qt.resolvedUrl(".").toString().replace(
|
|
||||||
"file://", "").replace(
|
|
||||||
"/Modules/Settings/",
|
|
||||||
"") + (isHyprland ? "/assets/hyprland.svg" : "/assets/niri.svg")
|
|
||||||
sourceSize: Qt.size(24, 24)
|
|
||||||
smooth: true
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
hoverEnabled: true
|
|
||||||
onEntered: parent.hovered = true
|
|
||||||
onExited: parent.hovered = false
|
|
||||||
onClicked: Qt.openUrlExternally(
|
|
||||||
isHyprland ? "https://hypr.land" : "https://github.com/YaLTeR/niri")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matrix button (only for Niri)
|
|
||||||
Item {
|
|
||||||
id: matrixButton
|
|
||||||
width: 30
|
|
||||||
height: 24
|
|
||||||
x: compositorButton.x + compositorButton.width + 4
|
|
||||||
visible: !isHyprland
|
|
||||||
|
|
||||||
property bool hovered: false
|
|
||||||
property string tooltipText: "niri Matrix Chat"
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Qt.resolvedUrl(".").toString().replace(
|
|
||||||
"file://", "").replace(
|
|
||||||
"/Modules/Settings/",
|
|
||||||
"") + "/assets/matrix-logo-white.svg"
|
|
||||||
sourceSize: Qt.size(28, 18)
|
|
||||||
smooth: true
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
colorization: 1
|
|
||||||
colorizationColor: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
hoverEnabled: true
|
|
||||||
onEntered: parent.hovered = true
|
|
||||||
onExited: parent.hovered = false
|
|
||||||
onClicked: Qt.openUrlExternally(
|
|
||||||
"https://matrix.to/#/#niri:matrix.org")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discord button
|
|
||||||
Item {
|
|
||||||
id: discordButton
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
x: isHyprland ? compositorButton.x + compositorButton.width + Theme.spacingM : matrixButton.x + matrixButton.width + Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
property bool hovered: false
|
|
||||||
property string tooltipText: isHyprland ? "Hyprland Discord Server" : "niri Discord Server"
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Qt.resolvedUrl(".").toString().replace(
|
|
||||||
"file://", "").replace(
|
|
||||||
"/Modules/Settings/",
|
|
||||||
"") + "/assets/discord.svg"
|
|
||||||
sourceSize: Qt.size(20, 20)
|
|
||||||
smooth: true
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
hoverEnabled: true
|
|
||||||
onEntered: parent.hovered = true
|
|
||||||
onExited: parent.hovered = false
|
|
||||||
onClicked: Qt.openUrlExternally(
|
|
||||||
isHyprland ? "https://discord.com/invite/hQ9XvMUjjr" : "https://discord.gg/vT8Sfjy7sx")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reddit button
|
|
||||||
Item {
|
|
||||||
id: redditButton
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
x: discordButton.x + discordButton.width + Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
property bool hovered: false
|
|
||||||
property string tooltipText: isHyprland ? "r/hyprland Subreddit" : "r/niri Subreddit"
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Qt.resolvedUrl(".").toString().replace(
|
|
||||||
"file://", "").replace(
|
|
||||||
"/Modules/Settings/",
|
|
||||||
"") + "/assets/reddit.svg"
|
|
||||||
sourceSize: Qt.size(20, 20)
|
|
||||||
smooth: true
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
hoverEnabled: true
|
|
||||||
onEntered: parent.hovered = true
|
|
||||||
onExited: parent.hovered = false
|
|
||||||
onClicked: Qt.openUrlExternally(
|
|
||||||
isHyprland ? "https://reddit.com/r/hyprland" : "https://reddit.com/r/niri")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Project Information
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: projectSection.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: projectSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "info"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("About")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr(`dms is a highly customizable, modern desktop shell with a <a href="https://m3.material.io/" style="text-decoration:none; color:${Theme.primary};">material 3 inspired</a> design.
|
|
||||||
<br /><br/>It is built with <a href="https://quickshell.org" style="text-decoration:none; color:${Theme.primary};">Quickshell</a>, a QT6 framework for building desktop shells, and <a href="https://go.dev" style="text-decoration:none; color:${Theme.primary};">Go</a>, a statically typed, compiled programming language.
|
|
||||||
`)
|
|
||||||
textFormat: Text.RichText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
linkColor: Theme.primary
|
|
||||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Technical Details
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: techSection.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: techSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "code"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Technical Details")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid {
|
|
||||||
width: parent.width
|
|
||||||
columns: 2
|
|
||||||
columnSpacing: Theme.spacingL
|
|
||||||
rowSpacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Framework:")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `<a href="https://quickshell.org" style="text-decoration:none; color:${Theme.primary};">Quickshell</a>`
|
|
||||||
linkColor: Theme.primary
|
|
||||||
textFormat: Text.RichText
|
|
||||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Language:")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("QML, JavaScript, Go")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Compositor:")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `<a href="https://github.com/YaLTeR/niri" style="text-decoration:none; color:${Theme.primary};">niri</a>`
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
linkColor: Theme.primary
|
|
||||||
textFormat: Text.RichText
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "&"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `<a href="https://github.com/hyprwm/Hyprland" style="text-decoration:none; color:${Theme.primary};">hyprland</a>`
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
linkColor: Theme.primary
|
|
||||||
textFormat: Text.RichText
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Github:")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `<a href="https://github.com/AvengeMedia/DankMaterialShell" style="text-decoration:none; color:${Theme.primary};">DankMaterialShell</a>`
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
linkColor: Theme.primary
|
|
||||||
textFormat: Text.RichText
|
|
||||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("- Support Us With a Star ⭐")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("System Monitoring:")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `<a href="https://github.com/AvengeMedia/dgop" style="text-decoration:none; color:${Theme.primary};">dgop</a>`
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
linkColor: Theme.primary
|
|
||||||
textFormat: Text.RichText
|
|
||||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("- Stateless System Monitoring")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Dank Suite:")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `<a href="https://danklinux.com" style="text-decoration:none; color:${Theme.primary};">danklinux.com</a>`
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
linkColor: Theme.primary
|
|
||||||
textFormat: Text.RichText
|
|
||||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Community tooltip - positioned absolutely above everything
|
|
||||||
Rectangle {
|
|
||||||
id: communityTooltip
|
|
||||||
parent: aboutTab
|
|
||||||
z: 1000
|
|
||||||
|
|
||||||
property var hoveredButton: {
|
|
||||||
if (compositorButton.hovered) return compositorButton
|
|
||||||
if (matrixButton.visible && matrixButton.hovered) return matrixButton
|
|
||||||
if (discordButton.hovered) return discordButton
|
|
||||||
if (redditButton.hovered) return redditButton
|
|
||||||
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: 0
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
|
|
||||||
x: hoveredButton ? hoveredButton.mapToItem(aboutTab, hoveredButton.width / 2, 0).x - width / 2 : 0
|
|
||||||
y: hoveredButton ? communityIcons.mapToItem(aboutTab, 0, 0).y - height - 8 : 0
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowOpacity: 0.15
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: communityTooltip.tooltipText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,564 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: dockTab
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// Dock Position
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: dockPositionSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: dockPositionSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "swap_vert"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: positionText
|
|
||||||
text: I18n.tr("Dock Position")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM - positionText.width - positionButtonGroup.width - Theme.spacingM * 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButtonGroup {
|
|
||||||
id: positionButtonGroup
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
model: ["Top", "Bottom", "Left", "Right"]
|
|
||||||
currentIndex: {
|
|
||||||
switch (SettingsData.dockPosition) {
|
|
||||||
case SettingsData.Position.Top: return 0
|
|
||||||
case SettingsData.Position.Bottom: return 1
|
|
||||||
case SettingsData.Position.Left: return 2
|
|
||||||
case SettingsData.Position.Right: return 3
|
|
||||||
default: return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onSelectionChanged: (index, selected) => {
|
|
||||||
if (selected) {
|
|
||||||
switch (index) {
|
|
||||||
case 0: SettingsData.setDockPosition(SettingsData.Position.Top); break
|
|
||||||
case 1: SettingsData.setDockPosition(SettingsData.Position.Bottom); break
|
|
||||||
case 2: SettingsData.setDockPosition(SettingsData.Position.Left); break
|
|
||||||
case 3: SettingsData.setDockPosition(SettingsData.Position.Right); break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dock Visibility Section
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: dockVisibilitySection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: dockVisibilitySection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "visibility_off"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- autoHideToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Auto-hide Dock")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Hide the dock when not in use and reveal it when hovering near the dock area")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: autoHideToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.dockAutoHide
|
|
||||||
onToggled: checked => {
|
|
||||||
SettingsData.setDockAutoHide(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "dock_to_bottom"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- enableToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Show Dock")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Display a dock with pinned and running applications that can be positioned at the top, bottom, left, or right edge of the screen")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: enableToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.showDock
|
|
||||||
onToggled: checked => {
|
|
||||||
SettingsData.setShowDock(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.2
|
|
||||||
visible: CompositorService.isNiri
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: CompositorService.isNiri
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "fullscreen"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- overviewToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Show on Overview")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Always show the dock when niri's overview is open")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: overviewToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.dockOpenOnOverview
|
|
||||||
onToggled: checked => {
|
|
||||||
SettingsData.setDockOpenOnOverview(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: I18n.tr("Group by App")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Group multiple windows of the same app together with a window count indicator")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: groupByAppToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.dockGroupByApp
|
|
||||||
onToggled: checked => {
|
|
||||||
SettingsData.setDockGroupByApp(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon Size Section
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: iconSizeSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 0
|
|
||||||
visible: SettingsData.showDock
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: iconSizeSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "photo_size_select_large"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Icon Size")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
value: SettingsData.dockIconSize
|
|
||||||
minimum: 24
|
|
||||||
maximum: 96
|
|
||||||
unit: ""
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
thumbOutlineColor: Theme.surfaceContainerHigh
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setDockIconSize(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dock Spacing Section
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: dockSpacingSection.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: dockSpacingSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "space_bar"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Spacing")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Padding")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
value: SettingsData.dockSpacing
|
|
||||||
minimum: 0
|
|
||||||
maximum: 32
|
|
||||||
unit: ""
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
thumbOutlineColor: Theme.surfaceContainerHigh
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setDockSpacing(
|
|
||||||
newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Height to Edge Gap (Exclusive Zone)")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
value: SettingsData.dockBottomGap
|
|
||||||
minimum: -100
|
|
||||||
maximum: 100
|
|
||||||
unit: ""
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
thumbOutlineColor: Theme.surfaceContainerHigh
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setDockBottomGap(
|
|
||||||
newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: I18n.tr("Dock Transparency")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
value: Math.round(SettingsData.dockTransparency * 100)
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: "%"
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
thumbOutlineColor: Theme.surfaceContainerHigh
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setDockTransparency(
|
|
||||||
newValue / 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,346 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var allWidgets: []
|
|
||||||
property string targetSection: ""
|
|
||||||
property string searchQuery: ""
|
|
||||||
property var filteredWidgets: []
|
|
||||||
property int selectedIndex: -1
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property Component widgetSelectionContent
|
|
||||||
property var parentModal: null
|
|
||||||
|
|
||||||
signal widgetSelected(string widgetId, string targetSection)
|
|
||||||
|
|
||||||
function updateFilteredWidgets() {
|
|
||||||
if (!searchQuery || searchQuery.length === 0) {
|
|
||||||
filteredWidgets = allWidgets.slice()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var filtered = []
|
|
||||||
var query = searchQuery.toLowerCase()
|
|
||||||
|
|
||||||
for (var i = 0; i < allWidgets.length; i++) {
|
|
||||||
var widget = allWidgets[i]
|
|
||||||
var text = widget.text ? widget.text.toLowerCase() : ""
|
|
||||||
var description = widget.description ? widget.description.toLowerCase() : ""
|
|
||||||
var id = widget.id ? widget.id.toLowerCase() : ""
|
|
||||||
|
|
||||||
if (text.indexOf(query) !== -1 ||
|
|
||||||
description.indexOf(query) !== -1 ||
|
|
||||||
id.indexOf(query) !== -1) {
|
|
||||||
filtered.push(widget)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredWidgets = filtered
|
|
||||||
selectedIndex = -1
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onAllWidgetsChanged: {
|
|
||||||
updateFilteredWidgets()
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (filteredWidgets.length === 0) return
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1, filteredWidgets.length - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (filteredWidgets.length === 0) return
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, -1)
|
|
||||||
if (selectedIndex === -1) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectWidget() {
|
|
||||||
if (selectedIndex >= 0 && selectedIndex < filteredWidgets.length) {
|
|
||||||
var widget = filteredWidgets[selectedIndex]
|
|
||||||
root.widgetSelected(widget.id, root.targetSection)
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
if (parentModal) {
|
|
||||||
parentModal.shouldHaveFocus = false
|
|
||||||
}
|
|
||||||
open()
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item && contentLoader.item.searchField) {
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close()
|
|
||||||
if (parentModal) {
|
|
||||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return parentModal.shouldBeVisible
|
|
||||||
})
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (parentModal && parentModal.modalFocusScope) {
|
|
||||||
parentModal.modalFocusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 500
|
|
||||||
height: 550
|
|
||||||
allowStacking: true
|
|
||||||
backgroundOpacity: 0
|
|
||||||
closeOnEscapeKey: false
|
|
||||||
onDialogClosed: () => {
|
|
||||||
allWidgets = []
|
|
||||||
targetSection = ""
|
|
||||||
searchQuery = ""
|
|
||||||
filteredWidgets = []
|
|
||||||
selectedIndex = -1
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
if (parentModal) {
|
|
||||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return parentModal.shouldBeVisible
|
|
||||||
})
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (parentModal && parentModal.modalFocusScope) {
|
|
||||||
parentModal.modalFocusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return hide()
|
|
||||||
}
|
|
||||||
content: widgetSelectionContent
|
|
||||||
|
|
||||||
widgetSelectionContent: Component {
|
|
||||||
FocusScope {
|
|
||||||
id: widgetKeyHandler
|
|
||||||
property alias searchField: searchField
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
root.selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
root.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
|
||||||
root.selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
|
||||||
root.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
|
||||||
root.selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
|
||||||
root.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
||||||
if (root.keyboardNavigationActive) {
|
|
||||||
root.selectWidget()
|
|
||||||
} else if (root.filteredWidgets.length > 0) {
|
|
||||||
var firstWidget = root.filteredWidgets[0]
|
|
||||||
root.widgetSelected(firstWidget.id, root.targetSection)
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.outline
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
onClicked: root.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
anchors.topMargin: Theme.spacingL + 30 // Space for close button
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "add_circle"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Add Widget to ") + root.targetSection + " Section"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Select a widget to add to the ") + root.targetSection.toLowerCase(
|
|
||||||
) + " section of the top bar. You can add multiple instances of the same widget if needed."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outline
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
width: parent.width
|
|
||||||
height: 48
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
backgroundColor: Theme.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.fontSizeMedium
|
|
||||||
placeholderText: ""
|
|
||||||
text: root.searchQuery
|
|
||||||
focus: true
|
|
||||||
ignoreLeftRightKeys: true
|
|
||||||
keyForwardTargets: [widgetKeyHandler]
|
|
||||||
onTextEdited: {
|
|
||||||
root.searchQuery = text
|
|
||||||
updateFilteredWidgets()
|
|
||||||
}
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up ||
|
|
||||||
((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: widgetList
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - y
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
model: root.filteredWidgets
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: widgetList.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
|
|
||||||
color: isSelected ? Theme.primarySelected :
|
|
||||||
widgetArea.containsMouse ? Theme.primaryHover : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.3)
|
|
||||||
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: isSelected ? 2 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: modelData.icon
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM * 3
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.text
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.description
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outline
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "add"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: widgetArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.widgetSelected(modelData.id,
|
|
||||||
root.targetSection)
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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.GenericCacheLocation).toString()
|
|
||||||
const baseDir = Paths.strip(cacheHome)
|
|
||||||
const outDir = baseDir + "/DankMaterialShell/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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
858
README.md
858
README.md
@@ -1,820 +1,192 @@
|
|||||||
# 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-bin)
|
||||||
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||||
[](https://ko-fi.com/avengemediallc)
|
[](https://ko-fi.com/danklinux)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/). Optimized for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors.
|
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
||||||
|
|
||||||
Features notifications, app launcher, wallpaper customization, and fully customizable with [plugins](https://github.com/AvengeMedia/dms-plugin-registry).
|
## Repository Structure
|
||||||
|
|
||||||
## Screenshots
|
This is a monorepo containing both the shell interface and the core backend services:
|
||||||
|
|
||||||
<div align="center">
|
```
|
||||||
<div style="max-width: 700px; margin: 0 auto;">
|
DankMaterialShell/
|
||||||
|
├── quickshell/ # QML-based shell interface
|
||||||
|
│ ├── Modules/ # UI components (panels, widgets, overlays)
|
||||||
|
│ ├── Services/ # System integration (audio, network, bluetooth)
|
||||||
|
│ ├── Widgets/ # Reusable UI controls
|
||||||
|
│ └── Common/ # Shared resources and themes
|
||||||
|
├── core/ # Go backend and CLI
|
||||||
|
│ ├── cmd/ # dms CLI and dankinstall binaries
|
||||||
|
│ ├── internal/ # System integration, IPC, distro support
|
||||||
|
│ └── pkg/ # Shared packages
|
||||||
|
├── distro/ # Distribution packaging
|
||||||
|
│ ├── fedora/ # Fedora RPM specs
|
||||||
|
│ ├── debian/ # Debian packaging
|
||||||
|
│ └── nix/ # NixOS/home-manager modules
|
||||||
|
└── flake.nix # Nix flake for declarative installation
|
||||||
|
```
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/40d2c56e-c1c9-4671-b04f-8f8b7b83b9ec
|
## See it in Action
|
||||||
|
|
||||||
</div>
|
|
||||||
</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/732c30de-5f4a-4a2b-a995-c8ab656cecd5" />
|
<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.
|
|
||||||
- A greeter
|
|
||||||
- A comprehensive plugin system for endless customization possibilities.
|
|
||||||
|
|
||||||
**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 particularly aims at supporting the **niri** and **Hyprland** compositors, but it does support more wayland compositors with a diminished feature set (no monitor off, workspace switcher, overview integration, etc.):
|
**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
|
|
||||||
sudo pacman -S niri
|
|
||||||
|
|
||||||
# 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/), [MangoWC](https://github.com/DreamMaoMao/mangowc), and [labwc](https://labwc.github.io/) 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
|
||||||
# Stable release
|
dms run # Start the shell
|
||||||
paru -S dms-shell-bin
|
|
||||||
# Latest -git
|
|
||||||
paru -S dms-shell-git
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Fedora - via COPR
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stable release
|
|
||||||
sudo dnf copr enable avengemedia/dms && sudo dnf install dms
|
|
||||||
# Latest -git
|
|
||||||
sudo dnf copr enable avengemedia/dms-git && sudo dnf install dms
|
|
||||||
```
|
|
||||||
|
|
||||||
#### NixOS - via flake
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix profile install github:AvengeMedia/DankMaterialShell
|
|
||||||
```
|
|
||||||
|
|
||||||
#### NixOS - via home-manager
|
|
||||||
|
|
||||||
To install using home-manager, you need to add this repo into your flake inputs:
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
dgop = {
|
|
||||||
url = "github:AvengeMedia/dgop";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
dms-cli = {
|
|
||||||
url = "github:AvengeMedia/danklinux";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
dankMaterialShell = {
|
|
||||||
url = "github:AvengeMedia/DankMaterialShell";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
inputs.dgop.follows = "dgop";
|
|
||||||
inputs.dms-cli.follows = "dms-cli";
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Then somewhere in your home-manager config, add this to the imports:
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
imports = [
|
|
||||||
inputs.dankMaterialShell.homeModules.dankMaterialShell.default
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
If you use Niri, the `niri` homeModule provides additional options for Niri integration, such as key bindings and spawn:
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
imports = [
|
|
||||||
inputs.dankMaterialShell.homeModules.dankMaterialShell.default
|
|
||||||
inputs.dankMaterialShell.homeModules.dankMaterialShell.niri
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> To use the `niri` homeModule, you must have `sobidoo/niri-flake` in your inputs:
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
niri = {
|
|
||||||
url = "github:sodiboo/niri-flake";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
And import it in home-manager:
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
imports = [
|
|
||||||
inputs.niri.homeModules.niri
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you can enable it with:
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
programs.dankMaterialShell.enable = true;
|
|
||||||
```
|
|
||||||
|
|
||||||
There are a lot of possible configurations that you can enable/disable in the flake, check [nix/default.nix](nix/default.nix) and [nix/niri.nix](nix/niri.nix) to see them all.
|
|
||||||
|
|
||||||
#### Other Distributions - via manual installation
|
|
||||||
|
|
||||||
#### 1. Install Quickshell (Varies by Distribution)
|
|
||||||
```bash
|
|
||||||
# Arch
|
|
||||||
paru -S quickshell-git
|
|
||||||
# Fedora
|
|
||||||
sudo dnf copr enable avengemedia/danklinux && 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
|
|
||||||
sudo curl -L "https://github.com/google/material-design-icons/raw/master/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.ttf" -o /usr/share/fonts/MaterialSymbolsRounded.ttf
|
|
||||||
```
|
|
||||||
#### 2.2 Install Inter Variable
|
|
||||||
```bash
|
|
||||||
sudo curl -L "https://github.com/rsms/inter/raw/refs/tags/v4.1/docs/font-files/InterVariable.ttf" -o /usr/share/fonts/InterVariable.ttf
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.3 Install Fira Code (monospace font)
|
|
||||||
```bash
|
|
||||||
sudo curl -L "https://github.com/tonsky/FiraCode/releases/latest/download/FiraCode-Regular.ttf" -o /usr/share/fonts/FiraCode-Regular.ttf
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.4 Refresh font cache
|
|
||||||
```bash
|
|
||||||
fc-cache -fv
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Install the shell
|
|
||||||
|
|
||||||
#### 3.1. Clone latest QML
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir ~/.config/quickshell && git clone https://github.com/AvengeMedia/DankMaterialShell.git ~/.config/quickshell/dms
|
|
||||||
```
|
|
||||||
|
|
||||||
**FOR Stable Version, Checkout the latest tag**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/.config/quickshell/dms
|
|
||||||
# You'll have to re-run this, to update to the latest version.
|
|
||||||
git checkout $(git describe --tags --abbrev=0)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 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"
|
|
||||||
```
|
|
||||||
**Note:** this is the latest *stable* dms CLI. If you are using QML/master (not pinned to a tag), then you may periodically be missing features, etc.
|
|
||||||
|
|
||||||
If preferred, you can build the dms-cli yourself (requires GO 1.24+)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/AvengeMedia/danklinux.git && cd danklinux
|
|
||||||
make && sudo make install
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 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 qt6-multimedia
|
|
||||||
paru -S matugen-bin dgop
|
|
||||||
|
|
||||||
# Fedora
|
|
||||||
sudo dnf install cava wl-clipboard brightnessctl qt6-qtmultimedia
|
|
||||||
sudo dnf copr enable avengemedia/danklinux && sudo dnf install cliphist ghostty hyprpicker material-symbols-fonts matugen
|
|
||||||
```
|
|
||||||
Note: by enabling and installing the avengemedia/dms copr above, these core dependencies will automatically be available for use.
|
|
||||||
|
|
||||||
*Other distros will just need to find sources for the above packages*
|
|
||||||
|
|
||||||
#### 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
|
|
||||||
- `qt6-multimedia`: System sound 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";
|
|
||||||
}
|
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
|
||||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
|
||||||
}
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### niri theming
|
|
||||||
|
|
||||||
If using a niri build newer than [3933903](https://github.com/YaLTeR/niri/commit/39339032cee3453faa54c361a38db6d83756f750), you can synchronize colors and gaps with the shell settings by adding the following to your niri config.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For colors
|
|
||||||
echo -e 'include "dms/colors.kdl"' >> ~/.config/niri/config.kdl
|
|
||||||
# For gaps, border widths, certain window rules
|
|
||||||
echo -e 'include "dms/layout.kdl"' >> ~/.config/niri/config.kdl
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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, Y, exec, dms ipc call dankdash wallpaper
|
|
||||||
bind = SUPER, C, exec, dms ipc call control-center toggle
|
|
||||||
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
|
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Greeter
|
|
||||||
|
|
||||||
You can install a matching [greetd](https://github.com/kennylevinsen/greetd) greeter, that will give you a greeter that matches the lock screen.
|
|
||||||
|
|
||||||
It's as simple as running `dms greeter install` in most cases, but more information is in the [Greetd module](Modules/Greetd/README.md)
|
|
||||||
|
|
||||||
## 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
|
||||||
|
- **[core/](core/)** - Go backend, CLI tools, and system integration
|
||||||
|
- **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
|
||||||
|
|
||||||
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 adw-gtk3
|
|
||||||
|
|
||||||
|
**Core + Dankinstall:**
|
||||||
```bash
|
```bash
|
||||||
# Arch
|
cd core
|
||||||
sudo pacman -S adw-gtk-theme
|
make # Build dms CLI
|
||||||
# Fedora
|
make dankinstall # Build installer
|
||||||
sudo dnf install adw-gtk3-theme
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In dms settings, navigate to Theme & Colors, and "apply GTK themes"
|
**Shell:**
|
||||||
|
```bash
|
||||||
|
quickshell -p quickshell/
|
||||||
|
```
|
||||||
|
|
||||||
This will create symlinks from `~/.config/gtk-3.0/4.0/dank-colors.css` to `~/.config/gtk-3.0/4.0/gtk.css` which enables theming.
|
**NixOS:**
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||||
|
|
||||||
#### QT: basic gtk3 based theme (Option 1)
|
# Use in home-manager or NixOS configuration
|
||||||
|
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
|
||||||
If you mostly use gtk apps, you'll probably be happy to just set the QT platform theme to gtk3.
|
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Plugins
|
|
||||||
|
|
||||||
[Plugin registry](https://github.com/AvengeMedia/dms-plugin-registry) - collection of available dms plugins.
|
|
||||||
|
|
||||||
dms features a plugin system - meaning you can create your own widgets and load other user widgets.
|
|
||||||
|
|
||||||
More comprehensive details available in the [PLUGINS](PLUGINS/README.md) - and examples [Emoji Plugin](PLUGINS/ExampleEmojiPlugin) and [Wallpaper Change Hook](PLUGINS/WallpaperWatcherDaemon) are available for reference.
|
|
||||||
|
|
||||||
Install an example plugin by:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir ~/.config/DankMaterialShell/plugins
|
|
||||||
cp -R ./PLUGINS/ExampleEmojiPlugin ~/.config/DankMaterialShell/plugins
|
|
||||||
```
|
|
||||||
|
|
||||||
**Only install plugins from TRUSTED sources.** Plugins execute QML and javascript at runtime, plugins from third parties should be reviewed before enabling them in dms.
|
|
||||||
|
|
||||||
### NixOS - via home-manager
|
|
||||||
|
|
||||||
Add the following to your home-manager config to install a plugin:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
programs.dankMaterialShell.plugins = {
|
|
||||||
ExampleEmojiPlugin.src = "${inputs.dankMaterialShell}/PLUGINS/ExampleEmojiPlugin";
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
||||||
- [Ly-sec](http://github.com/ly-sec) for awesome wallpaper effects among other things from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
- [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
||||||
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
|
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
|
||||||
- [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.
|
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property list<int> values: Array(6)
|
|
||||||
property int refCount: 0
|
|
||||||
property bool cavaAvailable: false
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: cavaCheck
|
|
||||||
|
|
||||||
command: ["which", "cava"]
|
|
||||||
running: false
|
|
||||||
onExited: exitCode => {
|
|
||||||
root.cavaAvailable = exitCode === 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
cavaCheck.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: cavaProcess
|
|
||||||
|
|
||||||
running: root.cavaAvailable && root.refCount > 0
|
|
||||||
command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`]
|
|
||||||
|
|
||||||
onRunningChanged: {
|
|
||||||
if (!running) {
|
|
||||||
root.values = Array(6).fill(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
onRead: data => {
|
|
||||||
if (root.refCount > 0 && data.trim()) {
|
|
||||||
let points = data.split(";").map(p => {
|
|
||||||
return parseInt(p.trim(), 10)
|
|
||||||
}).filter(p => {
|
|
||||||
return !isNaN(p)
|
|
||||||
})
|
|
||||||
if (points.length >= 6) {
|
|
||||||
root.values = points.slice(0, 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,372 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isHyprland: false
|
|
||||||
property bool isNiri: false
|
|
||||||
property 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: sortedToplevelsCache
|
|
||||||
property var sortedToplevelsCache: []
|
|
||||||
|
|
||||||
property bool _sortScheduled: false
|
|
||||||
property bool _refreshScheduled: false
|
|
||||||
property bool _hasRefreshedOnce: false
|
|
||||||
|
|
||||||
property var _coordCache: ({})
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: refreshTimer
|
|
||||||
interval: 40
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
try {
|
|
||||||
Hyprland.refreshToplevels()
|
|
||||||
} catch(e) {}
|
|
||||||
_refreshScheduled = false
|
|
||||||
_hasRefreshedOnce = true
|
|
||||||
scheduleSort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleSort() {
|
|
||||||
if (_sortScheduled) return
|
|
||||||
_sortScheduled = true
|
|
||||||
Qt.callLater(function() {
|
|
||||||
_sortScheduled = false
|
|
||||||
sortedToplevelsCache = computeSortedToplevels()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleRefresh() {
|
|
||||||
if (!isHyprland) return
|
|
||||||
if (_refreshScheduled) return
|
|
||||||
_refreshScheduled = true
|
|
||||||
refreshTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: ToplevelManager.toplevels
|
|
||||||
function onValuesChanged() { root.scheduleSort() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: Hyprland.toplevels
|
|
||||||
function onValuesChanged() {
|
|
||||||
root._hasRefreshedOnce = false
|
|
||||||
root.scheduleSort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: Hyprland.workspaces
|
|
||||||
function onValuesChanged() { root.scheduleSort() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: Hyprland
|
|
||||||
function onFocusedWorkspaceChanged() { root.scheduleSort() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
detectCompositor()
|
|
||||||
scheduleSort()
|
|
||||||
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeSortedToplevels() {
|
|
||||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
|
|
||||||
return []
|
|
||||||
|
|
||||||
if (useNiriSorting)
|
|
||||||
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
|
|
||||||
|
|
||||||
if (isHyprland)
|
|
||||||
return sortHyprlandToplevelsSafe()
|
|
||||||
|
|
||||||
return Array.from(ToplevelManager.toplevels.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
function _get(o, path, fallback) {
|
|
||||||
try {
|
|
||||||
let v = o
|
|
||||||
for (let i = 0; i < path.length; i++) {
|
|
||||||
if (v === null || v === undefined) return fallback
|
|
||||||
v = v[path[i]]
|
|
||||||
}
|
|
||||||
return (v === undefined || v === null) ? fallback : v
|
|
||||||
} catch (e) { return fallback }
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortHyprlandToplevelsSafe() {
|
|
||||||
if (!Hyprland.toplevels || !Hyprland.toplevels.values) return []
|
|
||||||
if (_refreshScheduled) return sortedToplevelsCache
|
|
||||||
|
|
||||||
const items = Array.from(Hyprland.toplevels.values)
|
|
||||||
|
|
||||||
function _get(o, path, fb) {
|
|
||||||
try {
|
|
||||||
let v = o
|
|
||||||
for (let k of path) { if (v == null) return fb; v = v[k] }
|
|
||||||
return (v == null) ? fb : v
|
|
||||||
} catch(e) { return fb }
|
|
||||||
}
|
|
||||||
|
|
||||||
let snap = []
|
|
||||||
let missingAnyPosition = false
|
|
||||||
let hasNewWindow = false
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const t = items[i]
|
|
||||||
if (!t) continue
|
|
||||||
|
|
||||||
const addr = t.address || ""
|
|
||||||
const li = t.lastIpcObject || null
|
|
||||||
|
|
||||||
const monName = _get(li, ["monitor"], null) ?? _get(t, ["monitor", "name"], "")
|
|
||||||
const monX = _get(t, ["monitor", "x"], Number.MAX_SAFE_INTEGER)
|
|
||||||
const monY = _get(t, ["monitor", "y"], Number.MAX_SAFE_INTEGER)
|
|
||||||
|
|
||||||
const wsId = _get(li, ["workspace", "id"], null) ?? _get(t, ["workspace", "id"], Number.MAX_SAFE_INTEGER)
|
|
||||||
|
|
||||||
const at = _get(li, ["at"], null)
|
|
||||||
let atX = (at !== null && at !== undefined && typeof at[0] === "number") ? at[0] : NaN
|
|
||||||
let atY = (at !== null && at !== undefined && typeof at[1] === "number") ? at[1] : NaN
|
|
||||||
|
|
||||||
if (!(atX === atX) || !(atY === atY)) {
|
|
||||||
const cached = _coordCache[addr]
|
|
||||||
if (cached) {
|
|
||||||
atX = cached.x
|
|
||||||
atY = cached.y
|
|
||||||
} else {
|
|
||||||
if (addr) hasNewWindow = true
|
|
||||||
missingAnyPosition = true
|
|
||||||
atX = 1e9
|
|
||||||
atY = 1e9
|
|
||||||
}
|
|
||||||
} else if (addr) {
|
|
||||||
_coordCache[addr] = { x: atX, y: atY }
|
|
||||||
}
|
|
||||||
|
|
||||||
const relX = Number.isFinite(monX) ? (atX - monX) : atX
|
|
||||||
const relY = Number.isFinite(monY) ? (atY - monY) : atY
|
|
||||||
|
|
||||||
snap.push({
|
|
||||||
monKey: String(monName),
|
|
||||||
monOrderX: Number.isFinite(monX) ? monX : Number.MAX_SAFE_INTEGER,
|
|
||||||
monOrderY: Number.isFinite(monY) ? monY : Number.MAX_SAFE_INTEGER,
|
|
||||||
wsId: (typeof wsId === "number") ? wsId : Number.MAX_SAFE_INTEGER,
|
|
||||||
x: relX,
|
|
||||||
y: relY,
|
|
||||||
title: t.title || "",
|
|
||||||
address: addr,
|
|
||||||
wayland: t.wayland
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingAnyPosition && hasNewWindow) {
|
|
||||||
_hasRefreshedOnce = false
|
|
||||||
scheduleRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = new Map()
|
|
||||||
for (const it of snap) {
|
|
||||||
const key = it.monKey + "::" + it.wsId
|
|
||||||
if (!groups.has(key)) groups.set(key, [])
|
|
||||||
groups.get(key).push(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
let groupList = []
|
|
||||||
for (const [key, arr] of groups) {
|
|
||||||
const repr = arr[0]
|
|
||||||
groupList.push({
|
|
||||||
key,
|
|
||||||
monKey: repr.monKey,
|
|
||||||
monOrderX: repr.monOrderX,
|
|
||||||
monOrderY: repr.monOrderY,
|
|
||||||
wsId: repr.wsId,
|
|
||||||
items: arr
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
groupList.sort((a, b) => {
|
|
||||||
if (a.monOrderX !== b.monOrderX) return a.monOrderX - b.monOrderX
|
|
||||||
if (a.monOrderY !== b.monOrderY) return a.monOrderY - b.monOrderY
|
|
||||||
if (a.monKey !== b.monKey) return a.monKey.localeCompare(b.monKey)
|
|
||||||
if (a.wsId !== b.wsId) return a.wsId - b.wsId
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const COLUMN_THRESHOLD = 48
|
|
||||||
const JITTER_Y = 6
|
|
||||||
|
|
||||||
let ordered = []
|
|
||||||
for (const g of groupList) {
|
|
||||||
const arr = g.items
|
|
||||||
|
|
||||||
const xs = arr.map(it => it.x).filter(x => Number.isFinite(x)).sort((a, b) => a - b)
|
|
||||||
let colCenters = []
|
|
||||||
if (xs.length > 0) {
|
|
||||||
for (const x of xs) {
|
|
||||||
if (colCenters.length === 0) {
|
|
||||||
colCenters.push(x)
|
|
||||||
} else {
|
|
||||||
const last = colCenters[colCenters.length - 1]
|
|
||||||
if (x - last >= COLUMN_THRESHOLD) {
|
|
||||||
colCenters.push(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
colCenters = [0]
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const it of arr) {
|
|
||||||
let bestCol = 0
|
|
||||||
let bestDist = Number.POSITIVE_INFINITY
|
|
||||||
for (let ci = 0; ci < colCenters.length; ci++) {
|
|
||||||
const d = Math.abs(it.x - colCenters[ci])
|
|
||||||
if (d < bestDist) {
|
|
||||||
bestDist = d
|
|
||||||
bestCol = ci
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it._col = bestCol
|
|
||||||
}
|
|
||||||
|
|
||||||
arr.sort((a, b) => {
|
|
||||||
if (a._col !== b._col) return a._col - b._col
|
|
||||||
|
|
||||||
const dy = a.y - b.y
|
|
||||||
if (Math.abs(dy) > JITTER_Y) return dy
|
|
||||||
|
|
||||||
if (a.title !== b.title) return a.title.localeCompare(b.title)
|
|
||||||
if (a.address !== b.address) return a.address.localeCompare(b.address)
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
ordered.push.apply(ordered, arr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ordered.map(x => x.wayland).filter(w => w !== null && w !== undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterCurrentWorkspace(toplevels, screen) {
|
|
||||||
if (useNiriSorting) return NiriService.filterCurrentWorkspace(toplevels, screen)
|
|
||||||
if (isHyprland) return filterHyprlandCurrentWorkspaceSafe(toplevels, screen)
|
|
||||||
return toplevels
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterHyprlandCurrentWorkspaceSafe(toplevels, screenName) {
|
|
||||||
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) return toplevels
|
|
||||||
|
|
||||||
let currentWorkspaceId = null
|
|
||||||
try {
|
|
||||||
const hy = Array.from(Hyprland.toplevels.values)
|
|
||||||
for (const t of hy) {
|
|
||||||
const mon = _get(t, ["monitor", "name"], "")
|
|
||||||
const wsId = _get(t, ["workspace", "id"], null)
|
|
||||||
const active = !!_get(t, ["activated"], false)
|
|
||||||
if (mon === screenName && wsId !== null) {
|
|
||||||
if (active) { currentWorkspaceId = wsId; break }
|
|
||||||
if (currentWorkspaceId === null) currentWorkspaceId = wsId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentWorkspaceId === null && Hyprland.workspaces) {
|
|
||||||
const wss = Array.from(Hyprland.workspaces.values)
|
|
||||||
const focusedId = _get(Hyprland, ["focusedWorkspace", "id"], null)
|
|
||||||
for (const ws of wss) {
|
|
||||||
const monName = _get(ws, ["monitor"], "")
|
|
||||||
const wsId = _get(ws, ["id"], null)
|
|
||||||
if (monName === screenName && wsId !== null) {
|
|
||||||
if (focusedId !== null && wsId === focusedId) { currentWorkspaceId = wsId; break }
|
|
||||||
if (currentWorkspaceId === null) currentWorkspaceId = wsId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("CompositorService: workspace snapshot failed:", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentWorkspaceId === null) return toplevels
|
|
||||||
|
|
||||||
// Map wayland → wsId snapshot
|
|
||||||
let map = new Map()
|
|
||||||
try {
|
|
||||||
const hy = Array.from(Hyprland.toplevels.values)
|
|
||||||
for (const t of hy) {
|
|
||||||
const wsId = _get(t, ["workspace", "id"], null)
|
|
||||||
if (t && t.wayland && wsId !== null) map.set(t.wayland, wsId)
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
return toplevels.filter(w => map.get(w) === currentWorkspaceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: compositorInitTimer
|
|
||||||
interval: 100
|
|
||||||
running: true
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
detectCompositor()
|
|
||||||
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCompositor() {
|
|
||||||
if (hyprlandSignature && hyprlandSignature.length > 0) {
|
|
||||||
isHyprland = true
|
|
||||||
isNiri = false
|
|
||||||
compositor = "hyprland"
|
|
||||||
console.log("CompositorService: Detected Hyprland")
|
|
||||||
try {
|
|
||||||
Hyprland.refreshToplevels()
|
|
||||||
} catch(e) {}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (niriSocket && niriSocket.length > 0) {
|
|
||||||
Proc.runCommand("niriSocketCheck", ["test", "-S", niriSocket], (output, exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
isNiri = true
|
|
||||||
isHyprland = false
|
|
||||||
compositor = "niri"
|
|
||||||
console.log("CompositorService: Detected Niri with socket:", niriSocket)
|
|
||||||
NiriService.generateNiriBinds()
|
|
||||||
} else {
|
|
||||||
isHyprland = false
|
|
||||||
isNiri = true
|
|
||||||
compositor = "niri"
|
|
||||||
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
} 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool dmsAvailable: false
|
|
||||||
property var capabilities: []
|
|
||||||
property int apiVersion: 0
|
|
||||||
readonly property int expectedApiVersion: 1
|
|
||||||
property var availablePlugins: []
|
|
||||||
property var installedPlugins: []
|
|
||||||
property bool isConnected: false
|
|
||||||
property bool isConnecting: false
|
|
||||||
property bool subscribeConnected: false
|
|
||||||
|
|
||||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
|
||||||
readonly property bool verboseLogs: Quickshell.env("DMS_VERBOSE_LOGS") === "1"
|
|
||||||
|
|
||||||
property var pendingRequests: ({})
|
|
||||||
property int requestIdCounter: 0
|
|
||||||
property bool shownOutdatedError: false
|
|
||||||
property string updateCommand: "dms update"
|
|
||||||
property bool checkingUpdateCommand: false
|
|
||||||
|
|
||||||
signal pluginsListReceived(var plugins)
|
|
||||||
signal installedPluginsReceived(var plugins)
|
|
||||||
signal searchResultsReceived(var plugins)
|
|
||||||
signal operationSuccess(string message)
|
|
||||||
signal operationError(string error)
|
|
||||||
signal connectionStateChanged()
|
|
||||||
|
|
||||||
signal networkStateUpdate(var data)
|
|
||||||
signal loginctlStateUpdate(var data)
|
|
||||||
signal loginctlEvent(var event)
|
|
||||||
signal capabilitiesReceived()
|
|
||||||
signal credentialsRequest(var data)
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (socketPath && socketPath.length > 0) {
|
|
||||||
detectUpdateCommand()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectUpdateCommand() {
|
|
||||||
checkingUpdateCommand = true
|
|
||||||
checkAurHelper.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSocketConnection() {
|
|
||||||
if (socketPath && socketPath.length > 0) {
|
|
||||||
testProcess.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: checkAurHelper
|
|
||||||
command: ["sh", "-c", "command -v paru || command -v yay"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const helper = text.trim()
|
|
||||||
if (helper.includes("paru")) {
|
|
||||||
checkDmsPackage.helper = "paru"
|
|
||||||
checkDmsPackage.running = true
|
|
||||||
} else if (helper.includes("yay")) {
|
|
||||||
checkDmsPackage.helper = "yay"
|
|
||||||
checkDmsPackage.running = true
|
|
||||||
} else {
|
|
||||||
updateCommand = "dms update"
|
|
||||||
checkingUpdateCommand = false
|
|
||||||
startSocketConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
updateCommand = "dms update"
|
|
||||||
checkingUpdateCommand = false
|
|
||||||
startSocketConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: checkDmsPackage
|
|
||||||
property string helper: ""
|
|
||||||
command: ["sh", "-c", "pacman -Qi dms-shell-git 2>/dev/null || pacman -Qi dms-shell-bin 2>/dev/null"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text.includes("dms-shell-git")) {
|
|
||||||
updateCommand = checkDmsPackage.helper + " -S dms-shell-git"
|
|
||||||
} else if (text.includes("dms-shell-bin")) {
|
|
||||||
updateCommand = checkDmsPackage.helper + " -S dms-shell-bin"
|
|
||||||
} else {
|
|
||||||
updateCommand = "dms update"
|
|
||||||
}
|
|
||||||
checkingUpdateCommand = false
|
|
||||||
startSocketConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
updateCommand = "dms update"
|
|
||||||
checkingUpdateCommand = false
|
|
||||||
startSocketConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: testProcess
|
|
||||||
command: ["test", "-S", root.socketPath]
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
root.dmsAvailable = true
|
|
||||||
connectSocket()
|
|
||||||
} else {
|
|
||||||
root.dmsAvailable = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectSocket() {
|
|
||||||
if (!dmsAvailable || isConnected || isConnecting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnecting = true
|
|
||||||
requestSocket.connected = true
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSocket {
|
|
||||||
id: requestSocket
|
|
||||||
path: root.socketPath
|
|
||||||
connected: false
|
|
||||||
|
|
||||||
onConnectionStateChanged: {
|
|
||||||
if (connected) {
|
|
||||||
root.isConnected = true
|
|
||||||
root.isConnecting = false
|
|
||||||
root.connectionStateChanged()
|
|
||||||
subscribeSocket.connected = true
|
|
||||||
} else {
|
|
||||||
root.isConnected = false
|
|
||||||
root.isConnecting = false
|
|
||||||
root.apiVersion = 0
|
|
||||||
root.capabilities = []
|
|
||||||
root.connectionStateChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser: SplitParser {
|
|
||||||
onRead: line => {
|
|
||||||
if (!line || line.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.verboseLogs) {
|
|
||||||
console.log("DMSService: Request socket <<", line)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(line)
|
|
||||||
handleResponse(response)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("DMSService: Failed to parse request response:", line, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSocket {
|
|
||||||
id: subscribeSocket
|
|
||||||
path: root.socketPath
|
|
||||||
connected: false
|
|
||||||
|
|
||||||
onConnectionStateChanged: {
|
|
||||||
root.subscribeConnected = connected
|
|
||||||
if (connected) {
|
|
||||||
sendSubscribeRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser: SplitParser {
|
|
||||||
onRead: line => {
|
|
||||||
if (!line || line.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.verboseLogs) {
|
|
||||||
console.log("DMSService: Subscribe socket <<", line)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(line)
|
|
||||||
handleSubscriptionEvent(response)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("DMSService: Failed to parse subscription event:", line, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendSubscribeRequest() {
|
|
||||||
const request = {
|
|
||||||
"method": "subscribe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verboseLogs) {
|
|
||||||
console.log("DMSService: Subscribing to all services")
|
|
||||||
}
|
|
||||||
subscribeSocket.send(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubscriptionEvent(response) {
|
|
||||||
if (response.error) {
|
|
||||||
if (response.error.includes("unknown method") && response.error.includes("subscribe")) {
|
|
||||||
if (!shownOutdatedError) {
|
|
||||||
console.error("DMSService: Server does not support subscribe method")
|
|
||||||
ToastService.showError(
|
|
||||||
I18n.tr("DMS out of date"),
|
|
||||||
I18n.tr("To update, run the following command:"),
|
|
||||||
updateCommand
|
|
||||||
)
|
|
||||||
shownOutdatedError = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.result) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const service = response.result.service
|
|
||||||
const data = response.result.data
|
|
||||||
|
|
||||||
if (service === "server") {
|
|
||||||
apiVersion = data.apiVersion || 0
|
|
||||||
capabilities = data.capabilities || []
|
|
||||||
|
|
||||||
console.log("DMSService: Connected (API v" + apiVersion + ") -", JSON.stringify(capabilities))
|
|
||||||
|
|
||||||
if (apiVersion < expectedApiVersion) {
|
|
||||||
ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")")
|
|
||||||
}
|
|
||||||
|
|
||||||
capabilitiesReceived()
|
|
||||||
} else if (service === "network") {
|
|
||||||
networkStateUpdate(data)
|
|
||||||
} else if (service === "network.credentials") {
|
|
||||||
credentialsRequest(data)
|
|
||||||
} else if (service === "loginctl") {
|
|
||||||
if (data.event) {
|
|
||||||
loginctlEvent(data)
|
|
||||||
} else {
|
|
||||||
loginctlStateUpdate(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendRequest(method, params, callback) {
|
|
||||||
if (!isConnected) {
|
|
||||||
if (callback) {
|
|
||||||
callback({
|
|
||||||
"error": "not connected to DMS socket"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestIdCounter++
|
|
||||||
const id = Date.now() + requestIdCounter
|
|
||||||
const request = {
|
|
||||||
"id": id,
|
|
||||||
"method": method
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params) {
|
|
||||||
request.params = params
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
pendingRequests[id] = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
requestSocket.send(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResponse(response) {
|
|
||||||
const callback = pendingRequests[response.id]
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
delete pendingRequests[response.id]
|
|
||||||
callback(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ping(callback) {
|
|
||||||
sendRequest("ping", null, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
function listPlugins(callback) {
|
|
||||||
sendRequest("plugins.list", null, response => {
|
|
||||||
if (response.result) {
|
|
||||||
availablePlugins = response.result
|
|
||||||
pluginsListReceived(response.result)
|
|
||||||
}
|
|
||||||
if (callback) {
|
|
||||||
callback(response)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function listInstalled(callback) {
|
|
||||||
sendRequest("plugins.listInstalled", null, response => {
|
|
||||||
if (response.result) {
|
|
||||||
installedPlugins = response.result
|
|
||||||
installedPluginsReceived(response.result)
|
|
||||||
}
|
|
||||||
if (callback) {
|
|
||||||
callback(response)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function search(query, category, compositor, capability, callback) {
|
|
||||||
const params = {
|
|
||||||
"query": query
|
|
||||||
}
|
|
||||||
if (category) {
|
|
||||||
params.category = category
|
|
||||||
}
|
|
||||||
if (compositor) {
|
|
||||||
params.compositor = compositor
|
|
||||||
}
|
|
||||||
if (capability) {
|
|
||||||
params.capability = capability
|
|
||||||
}
|
|
||||||
|
|
||||||
sendRequest("plugins.search", params, response => {
|
|
||||||
if (response.result) {
|
|
||||||
searchResultsReceived(response.result)
|
|
||||||
}
|
|
||||||
if (callback) {
|
|
||||||
callback(response)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function install(pluginName, callback) {
|
|
||||||
sendRequest("plugins.install", {
|
|
||||||
"name": pluginName
|
|
||||||
}, response => {
|
|
||||||
if (callback) {
|
|
||||||
callback(response)
|
|
||||||
}
|
|
||||||
if (!response.error) {
|
|
||||||
listInstalled()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function uninstall(pluginName, callback) {
|
|
||||||
sendRequest("plugins.uninstall", {
|
|
||||||
"name": pluginName
|
|
||||||
}, response => {
|
|
||||||
if (callback) {
|
|
||||||
callback(response)
|
|
||||||
}
|
|
||||||
if (!response.error) {
|
|
||||||
listInstalled()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(pluginName, callback) {
|
|
||||||
sendRequest("plugins.update", {
|
|
||||||
"name": pluginName
|
|
||||||
}, response => {
|
|
||||||
if (callback) {
|
|
||||||
callback(response)
|
|
||||||
}
|
|
||||||
if (!response.error) {
|
|
||||||
listInstalled()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function lockSession(callback) {
|
|
||||||
sendRequest("loginctl.lock", null, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
function unlockSession(callback) {
|
|
||||||
sendRequest("loginctl.unlock", null, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtCore
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Services/", "")
|
|
||||||
property string scriptPath: `${shellDir}/scripts/hyprland_keybinds.py`
|
|
||||||
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
|
||||||
readonly property string _configDir: Paths.strip(_configUrl)
|
|
||||||
property string hyprConfigPath: `${_configDir}/hypr`
|
|
||||||
property var keybinds: ({"children": [], "keybinds": []})
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: getKeybinds
|
|
||||||
running: true
|
|
||||||
command: [root.scriptPath, "--path", root.hyprConfigPath]
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
onRead: data => {
|
|
||||||
try {
|
|
||||||
root.keybinds = JSON.parse(data)
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[HyprKeybindsService] Error parsing keybinds:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload() {
|
|
||||||
getKeybinds.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property bool idleMonitorAvailable: {
|
|
||||||
try {
|
|
||||||
return typeof IdleMonitor !== "undefined"
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool enabled: true
|
|
||||||
property bool respectInhibitors: true
|
|
||||||
property bool _enableGate: true
|
|
||||||
|
|
||||||
readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn
|
|
||||||
readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout
|
|
||||||
readonly property int lockTimeout: isOnBattery ? SettingsData.batteryLockTimeout : SettingsData.acLockTimeout
|
|
||||||
readonly property int suspendTimeout: isOnBattery ? SettingsData.batterySuspendTimeout : SettingsData.acSuspendTimeout
|
|
||||||
readonly property int hibernateTimeout: isOnBattery ? SettingsData.batteryHibernateTimeout : SettingsData.acHibernateTimeout
|
|
||||||
|
|
||||||
onMonitorTimeoutChanged: _rearmIdleMonitors()
|
|
||||||
onLockTimeoutChanged: _rearmIdleMonitors()
|
|
||||||
onSuspendTimeoutChanged: _rearmIdleMonitors()
|
|
||||||
onHibernateTimeoutChanged: _rearmIdleMonitors()
|
|
||||||
|
|
||||||
function _rearmIdleMonitors() {
|
|
||||||
_enableGate = false
|
|
||||||
Qt.callLater(() => { _enableGate = true })
|
|
||||||
}
|
|
||||||
|
|
||||||
signal lockRequested()
|
|
||||||
signal requestMonitorOff()
|
|
||||||
signal requestMonitorOn()
|
|
||||||
signal requestSuspend()
|
|
||||||
signal requestHibernate()
|
|
||||||
|
|
||||||
property var monitorOffMonitor: null
|
|
||||||
property var lockMonitor: null
|
|
||||||
property var suspendMonitor: null
|
|
||||||
property var hibernateMonitor: null
|
|
||||||
|
|
||||||
function wake() {
|
|
||||||
requestMonitorOn()
|
|
||||||
}
|
|
||||||
|
|
||||||
function createIdleMonitors() {
|
|
||||||
if (!idleMonitorAvailable) {
|
|
||||||
console.log("IdleService: IdleMonitor not available, skipping creation")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const qmlString = `
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell.Wayland
|
|
||||||
|
|
||||||
IdleMonitor {
|
|
||||||
enabled: false
|
|
||||||
respectInhibitors: true
|
|
||||||
timeout: 0
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
monitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.MonitorOffMonitor")
|
|
||||||
monitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.monitorTimeout > 0)
|
|
||||||
monitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors)
|
|
||||||
monitorOffMonitor.timeout = Qt.binding(() => root.monitorTimeout)
|
|
||||||
monitorOffMonitor.isIdleChanged.connect(function() {
|
|
||||||
if (monitorOffMonitor.isIdle) {
|
|
||||||
root.requestMonitorOff()
|
|
||||||
} else {
|
|
||||||
root.requestMonitorOn()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
lockMonitor = Qt.createQmlObject(qmlString, root, "IdleService.LockMonitor")
|
|
||||||
lockMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.lockTimeout > 0)
|
|
||||||
lockMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors)
|
|
||||||
lockMonitor.timeout = Qt.binding(() => root.lockTimeout)
|
|
||||||
lockMonitor.isIdleChanged.connect(function() {
|
|
||||||
if (lockMonitor.isIdle) {
|
|
||||||
root.lockRequested()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
suspendMonitor = Qt.createQmlObject(qmlString, root, "IdleService.SuspendMonitor")
|
|
||||||
suspendMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.suspendTimeout > 0)
|
|
||||||
suspendMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors)
|
|
||||||
suspendMonitor.timeout = Qt.binding(() => root.suspendTimeout)
|
|
||||||
suspendMonitor.isIdleChanged.connect(function() {
|
|
||||||
if (suspendMonitor.isIdle) {
|
|
||||||
root.requestSuspend()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
hibernateMonitor = Qt.createQmlObject(qmlString, root, "IdleService.HibernateMonitor")
|
|
||||||
hibernateMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.hibernateTimeout > 0)
|
|
||||||
hibernateMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors)
|
|
||||||
hibernateMonitor.timeout = Qt.binding(() => root.hibernateTimeout)
|
|
||||||
hibernateMonitor.isIdleChanged.connect(function() {
|
|
||||||
if (hibernateMonitor.isIdle) {
|
|
||||||
root.requestHibernate()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("IdleService: Error creating IdleMonitors:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onRequestMonitorOff() {
|
|
||||||
CompositorService.powerOffMonitors()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRequestMonitorOn() {
|
|
||||||
CompositorService.powerOnMonitors()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRequestSuspend() {
|
|
||||||
SessionService.suspend()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRequestHibernate() {
|
|
||||||
SessionService.hibernate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionService
|
|
||||||
function onPrepareForSleep() {
|
|
||||||
if (SettingsData.lockBeforeSuspend) {
|
|
||||||
root.lockRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!idleMonitorAvailable) {
|
|
||||||
console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.")
|
|
||||||
} else {
|
|
||||||
console.log("IdleService: Initialized with idle monitoring support")
|
|
||||||
createIdleMonitors()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,733 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool networkAvailable: false
|
|
||||||
|
|
||||||
property string networkStatus: "disconnected"
|
|
||||||
property string primaryConnection: ""
|
|
||||||
|
|
||||||
property string ethernetIP: ""
|
|
||||||
property string ethernetInterface: ""
|
|
||||||
property bool ethernetConnected: false
|
|
||||||
property string ethernetConnectionUuid: ""
|
|
||||||
|
|
||||||
property var wiredConnections: []
|
|
||||||
|
|
||||||
property string wifiIP: ""
|
|
||||||
property string wifiInterface: ""
|
|
||||||
property bool wifiConnected: false
|
|
||||||
property bool wifiEnabled: true
|
|
||||||
property string wifiConnectionUuid: ""
|
|
||||||
property string wifiDevicePath: ""
|
|
||||||
property string activeAccessPointPath: ""
|
|
||||||
|
|
||||||
property string currentWifiSSID: ""
|
|
||||||
property int wifiSignalStrength: 0
|
|
||||||
property var wifiNetworks: []
|
|
||||||
property var savedConnections: []
|
|
||||||
property var ssidToConnectionName: ({})
|
|
||||||
property var wifiSignalIcon: {
|
|
||||||
if (!wifiConnected || networkStatus !== "wifi") {
|
|
||||||
return "wifi_off"
|
|
||||||
}
|
|
||||||
if (wifiSignalStrength >= 50) {
|
|
||||||
return "wifi"
|
|
||||||
}
|
|
||||||
if (wifiSignalStrength >= 25) {
|
|
||||||
return "wifi_2_bar"
|
|
||||||
}
|
|
||||||
return "wifi_1_bar"
|
|
||||||
}
|
|
||||||
|
|
||||||
property string userPreference: "auto"
|
|
||||||
property bool isConnecting: false
|
|
||||||
property string connectingSSID: ""
|
|
||||||
property string connectionError: ""
|
|
||||||
|
|
||||||
property bool isScanning: false
|
|
||||||
property bool autoScan: false
|
|
||||||
|
|
||||||
property bool wifiAvailable: true
|
|
||||||
property bool wifiToggling: false
|
|
||||||
property bool changingPreference: false
|
|
||||||
property string targetPreference: ""
|
|
||||||
property var savedWifiNetworks: []
|
|
||||||
property string connectionStatus: ""
|
|
||||||
property string lastConnectionError: ""
|
|
||||||
property bool passwordDialogShouldReopen: false
|
|
||||||
property bool autoRefreshEnabled: false
|
|
||||||
property string wifiPassword: ""
|
|
||||||
property string forgetSSID: ""
|
|
||||||
|
|
||||||
property string networkInfoSSID: ""
|
|
||||||
property string networkInfoDetails: ""
|
|
||||||
property bool networkInfoLoading: false
|
|
||||||
|
|
||||||
property string networkWiredInfoUUID: ""
|
|
||||||
property string networkWiredInfoDetails: ""
|
|
||||||
property bool networkWiredInfoLoading: false
|
|
||||||
|
|
||||||
property int refCount: 0
|
|
||||||
property bool stateInitialized: false
|
|
||||||
|
|
||||||
property string credentialsToken: ""
|
|
||||||
property string credentialsSSID: ""
|
|
||||||
property string credentialsSetting: ""
|
|
||||||
property var credentialsFields: []
|
|
||||||
property var credentialsHints: []
|
|
||||||
property string credentialsReason: ""
|
|
||||||
property bool credentialsRequested: false
|
|
||||||
|
|
||||||
property string pendingConnectionSSID: ""
|
|
||||||
property var pendingConnectionStartTime: 0
|
|
||||||
property bool wasConnecting: false
|
|
||||||
|
|
||||||
signal networksUpdated
|
|
||||||
signal connectionChanged
|
|
||||||
signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason)
|
|
||||||
|
|
||||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
root.userPreference = SettingsData.networkPreference
|
|
||||||
if (socketPath && socketPath.length > 0) {
|
|
||||||
checkDMSCapabilities()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DMSService
|
|
||||||
|
|
||||||
function onNetworkStateUpdate(data) {
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
const networksCount = data.wifiNetworks?.length ?? "null"
|
|
||||||
console.log("NetworkManagerService: Subscription update received, networks:", networksCount)
|
|
||||||
}
|
|
||||||
updateState(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DMSService
|
|
||||||
|
|
||||||
function onConnectionStateChanged() {
|
|
||||||
if (DMSService.isConnected) {
|
|
||||||
checkDMSCapabilities()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DMSService
|
|
||||||
enabled: DMSService.isConnected
|
|
||||||
|
|
||||||
function onCapabilitiesChanged() {
|
|
||||||
checkDMSCapabilities()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCredentialsRequest(data) {
|
|
||||||
handleCredentialsRequest(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkDMSCapabilities() {
|
|
||||||
if (!DMSService.isConnected) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DMSService.capabilities.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
networkAvailable = DMSService.capabilities.includes("network")
|
|
||||||
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Network available:", networkAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (networkAvailable && !stateInitialized) {
|
|
||||||
stateInitialized = true
|
|
||||||
getState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCredentialsRequest(data) {
|
|
||||||
credentialsToken = data.token || ""
|
|
||||||
credentialsSSID = data.ssid || ""
|
|
||||||
credentialsSetting = data.setting || "802-11-wireless-security"
|
|
||||||
credentialsFields = data.fields || ["psk"]
|
|
||||||
credentialsHints = data.hints || []
|
|
||||||
credentialsReason = data.reason || "Credentials required"
|
|
||||||
credentialsRequested = true
|
|
||||||
|
|
||||||
credentialsNeeded(credentialsToken, credentialsSSID, credentialsSetting, credentialsFields, credentialsHints, credentialsReason)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRef() {
|
|
||||||
refCount++
|
|
||||||
if (refCount === 1 && networkAvailable) {
|
|
||||||
startAutoScan()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeRef() {
|
|
||||||
refCount = Math.max(0, refCount - 1)
|
|
||||||
if (refCount === 0) {
|
|
||||||
stopAutoScan()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool initialStateFetched: false
|
|
||||||
|
|
||||||
function getState() {
|
|
||||||
if (!networkAvailable) return
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.getState", null, response => {
|
|
||||||
if (response.result) {
|
|
||||||
updateState(response.result)
|
|
||||||
if (!initialStateFetched && response.result.wifiEnabled && (!response.result.wifiNetworks || response.result.wifiNetworks.length === 0)) {
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Initial state has no networks, triggering scan")
|
|
||||||
}
|
|
||||||
initialStateFetched = true
|
|
||||||
Qt.callLater(() => scanWifi())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateState(state) {
|
|
||||||
const previousConnecting = isConnecting
|
|
||||||
const previousConnectingSSID = connectingSSID
|
|
||||||
|
|
||||||
networkStatus = state.networkStatus || "disconnected"
|
|
||||||
primaryConnection = state.primaryConnection || ""
|
|
||||||
|
|
||||||
ethernetIP = state.ethernetIP || ""
|
|
||||||
ethernetInterface = state.ethernetDevice || ""
|
|
||||||
ethernetConnected = state.ethernetConnected || false
|
|
||||||
ethernetConnectionUuid = state.ethernetConnectionUuid || ""
|
|
||||||
|
|
||||||
wiredConnections = state.wiredConnections || []
|
|
||||||
|
|
||||||
wifiIP = state.wifiIP || ""
|
|
||||||
wifiInterface = state.wifiDevice || ""
|
|
||||||
wifiConnected = state.wifiConnected || false
|
|
||||||
wifiEnabled = state.wifiEnabled !== undefined ? state.wifiEnabled : true
|
|
||||||
wifiConnectionUuid = state.wifiConnectionUuid || ""
|
|
||||||
wifiDevicePath = state.wifiDevicePath || ""
|
|
||||||
activeAccessPointPath = state.activeAccessPointPath || ""
|
|
||||||
|
|
||||||
currentWifiSSID = state.wifiSSID || ""
|
|
||||||
wifiSignalStrength = state.wifiSignal || 0
|
|
||||||
|
|
||||||
if (state.wifiNetworks) {
|
|
||||||
wifiNetworks = state.wifiNetworks
|
|
||||||
|
|
||||||
const saved = []
|
|
||||||
const mapping = {}
|
|
||||||
for (const network of state.wifiNetworks) {
|
|
||||||
if (network.saved) {
|
|
||||||
saved.push({
|
|
||||||
ssid: network.ssid,
|
|
||||||
saved: true
|
|
||||||
})
|
|
||||||
mapping[network.ssid] = network.ssid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
savedConnections = saved
|
|
||||||
savedWifiNetworks = saved
|
|
||||||
ssidToConnectionName = mapping
|
|
||||||
|
|
||||||
networksUpdated()
|
|
||||||
}
|
|
||||||
|
|
||||||
userPreference = state.preference || "auto"
|
|
||||||
isConnecting = state.isConnecting || false
|
|
||||||
connectingSSID = state.connectingSSID || ""
|
|
||||||
connectionError = state.lastError || ""
|
|
||||||
lastConnectionError = state.lastError || ""
|
|
||||||
|
|
||||||
if (pendingConnectionSSID) {
|
|
||||||
if (wifiConnected && currentWifiSSID === pendingConnectionSSID && wifiIP) {
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
const elapsed = Date.now() - pendingConnectionStartTime
|
|
||||||
console.log("NetworkManagerService: Successfully connected to", pendingConnectionSSID, "in", elapsed, "ms")
|
|
||||||
}
|
|
||||||
ToastService.showInfo(`Connected to ${pendingConnectionSSID}`)
|
|
||||||
|
|
||||||
if (userPreference === "wifi" || userPreference === "auto") {
|
|
||||||
setConnectionPriority("wifi")
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingConnectionSSID = ""
|
|
||||||
connectionStatus = "connected"
|
|
||||||
} else if (previousConnecting && !isConnecting && !wifiConnected) {
|
|
||||||
const elapsed = Date.now() - pendingConnectionStartTime
|
|
||||||
|
|
||||||
if (elapsed < 5000) {
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Quick connection failure, likely authentication error")
|
|
||||||
}
|
|
||||||
connectionStatus = "invalid_password"
|
|
||||||
} else {
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Connection failed for", pendingConnectionSSID)
|
|
||||||
}
|
|
||||||
if (connectionError === "connection-failed") {
|
|
||||||
ToastService.showError(I18n.tr("Connection failed. Check password and try again."))
|
|
||||||
} else if (connectionError) {
|
|
||||||
ToastService.showError(I18n.tr("Failed to connect to ") + pendingConnectionSSID)
|
|
||||||
}
|
|
||||||
connectionStatus = "failed"
|
|
||||||
pendingConnectionSSID = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wasConnecting = isConnecting
|
|
||||||
|
|
||||||
connectionChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectToSpecificWiredConfig(uuid) {
|
|
||||||
if (!networkAvailable || isConnecting) return
|
|
||||||
|
|
||||||
isConnecting = true
|
|
||||||
connectionError = ""
|
|
||||||
connectionStatus = "connecting"
|
|
||||||
|
|
||||||
const params = { uuid: uuid }
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.ethernet.connect.config", params, response => {
|
|
||||||
if (response.error) {
|
|
||||||
connectionError = response.error
|
|
||||||
lastConnectionError = response.error
|
|
||||||
connectionStatus = "failed"
|
|
||||||
ToastService.showError(I18n.tr("Failed to activate configuration"))
|
|
||||||
} else {
|
|
||||||
connectionError = ""
|
|
||||||
connectionStatus = "connected"
|
|
||||||
ToastService.showInfo(I18n.tr("Configuration activated"))
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnecting = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanWifi() {
|
|
||||||
if (!networkAvailable || isScanning || !wifiEnabled) return
|
|
||||||
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Starting WiFi scan...")
|
|
||||||
}
|
|
||||||
isScanning = true
|
|
||||||
DMSService.sendRequest("network.wifi.scan", null, response => {
|
|
||||||
isScanning = false
|
|
||||||
if (response.error) {
|
|
||||||
console.warn("NetworkManagerService: WiFi scan failed:", response.error)
|
|
||||||
} else {
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Scan completed")
|
|
||||||
}
|
|
||||||
Qt.callLater(() => getState())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanWifiNetworks() {
|
|
||||||
scanWifi()
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
|
||||||
if (!networkAvailable || isConnecting) return
|
|
||||||
|
|
||||||
pendingConnectionSSID = ssid
|
|
||||||
pendingConnectionStartTime = Date.now()
|
|
||||||
connectionError = ""
|
|
||||||
connectionStatus = "connecting"
|
|
||||||
credentialsRequested = false
|
|
||||||
|
|
||||||
const params = { ssid: ssid }
|
|
||||||
|
|
||||||
if (DMSService.apiVersion >= 7) {
|
|
||||||
if (password || username) {
|
|
||||||
params.password = password
|
|
||||||
if (username) params.username = username
|
|
||||||
if (anonymousIdentity) params.anonymousIdentity = anonymousIdentity
|
|
||||||
if (domainSuffixMatch) params.domainSuffixMatch = domainSuffixMatch
|
|
||||||
params.interactive = false
|
|
||||||
} else {
|
|
||||||
params.interactive = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (password) params.password = password
|
|
||||||
if (username) params.username = username
|
|
||||||
if (anonymousIdentity) params.anonymousIdentity = anonymousIdentity
|
|
||||||
if (domainSuffixMatch) params.domainSuffixMatch = domainSuffixMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.wifi.connect", params, response => {
|
|
||||||
if (response.error) {
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Connection request failed:", response.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
connectionError = response.error
|
|
||||||
lastConnectionError = response.error
|
|
||||||
pendingConnectionSSID = ""
|
|
||||||
connectionStatus = "failed"
|
|
||||||
ToastService.showError(I18n.tr("Failed to start connection to ") + ssid)
|
|
||||||
} else {
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Connection request sent for", ssid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectWifi() {
|
|
||||||
if (!networkAvailable || !wifiInterface) return
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.wifi.disconnect", null, response => {
|
|
||||||
if (response.error) {
|
|
||||||
ToastService.showError(I18n.tr("Failed to disconnect WiFi"))
|
|
||||||
} else {
|
|
||||||
ToastService.showInfo(I18n.tr("Disconnected from WiFi"))
|
|
||||||
currentWifiSSID = ""
|
|
||||||
connectionStatus = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitCredentials(token, secrets, save) {
|
|
||||||
if (!networkAvailable || DMSService.apiVersion < 7) return
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
token: token,
|
|
||||||
secrets: secrets,
|
|
||||||
save: save || false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Submitting credentials for token", token)
|
|
||||||
}
|
|
||||||
|
|
||||||
credentialsRequested = false
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.credentials.submit", params, response => {
|
|
||||||
if (response.error) {
|
|
||||||
console.warn("NetworkManagerService: Failed to submit credentials:", response.error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelCredentials(token) {
|
|
||||||
if (!networkAvailable || DMSService.apiVersion < 7) return
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
token: token,
|
|
||||||
cancel: true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DMSService.verboseLogs) {
|
|
||||||
console.log("NetworkManagerService: Cancelling credentials for token", token)
|
|
||||||
}
|
|
||||||
|
|
||||||
credentialsRequested = false
|
|
||||||
pendingConnectionSSID = ""
|
|
||||||
connectionStatus = "cancelled"
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.credentials.submit", params, response => {
|
|
||||||
if (response.error) {
|
|
||||||
console.warn("NetworkManagerService: Failed to cancel credentials:", response.error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function forgetWifiNetwork(ssid) {
|
|
||||||
if (!networkAvailable) return
|
|
||||||
|
|
||||||
forgetSSID = ssid
|
|
||||||
DMSService.sendRequest("network.wifi.forget", { ssid: ssid }, response => {
|
|
||||||
if (response.error) {
|
|
||||||
console.warn("Failed to forget network:", response.error)
|
|
||||||
} else {
|
|
||||||
ToastService.showInfo(I18n.tr("Forgot network ") + ssid)
|
|
||||||
|
|
||||||
savedConnections = savedConnections.filter(s => s.ssid !== ssid)
|
|
||||||
savedWifiNetworks = savedWifiNetworks.filter(s => s.ssid !== ssid)
|
|
||||||
|
|
||||||
const updated = [...wifiNetworks]
|
|
||||||
for (const network of updated) {
|
|
||||||
if (network.ssid === ssid) {
|
|
||||||
network.saved = false
|
|
||||||
if (network.connected) {
|
|
||||||
network.connected = false
|
|
||||||
currentWifiSSID = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wifiNetworks = updated
|
|
||||||
networksUpdated()
|
|
||||||
}
|
|
||||||
forgetSSID = ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleWifiRadio() {
|
|
||||||
if (!networkAvailable || wifiToggling) return
|
|
||||||
|
|
||||||
wifiToggling = true
|
|
||||||
DMSService.sendRequest("network.wifi.toggle", null, response => {
|
|
||||||
wifiToggling = false
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
console.warn("Failed to toggle WiFi:", response.error)
|
|
||||||
} else if (response.result) {
|
|
||||||
wifiEnabled = response.result.enabled
|
|
||||||
ToastService.showInfo(wifiEnabled ? I18n.tr("WiFi enabled") : I18n.tr("WiFi disabled"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableWifiDevice() {
|
|
||||||
if (!networkAvailable) return
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.wifi.enable", null, response => {
|
|
||||||
if (response.error) {
|
|
||||||
ToastService.showError(I18n.tr("Failed to enable WiFi"))
|
|
||||||
} else {
|
|
||||||
ToastService.showInfo(I18n.tr("WiFi enabled"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNetworkPreference(preference) {
|
|
||||||
if (!networkAvailable) return
|
|
||||||
|
|
||||||
userPreference = preference
|
|
||||||
changingPreference = true
|
|
||||||
targetPreference = preference
|
|
||||||
SettingsData.setNetworkPreference(preference)
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.preference.set", { preference: preference }, response => {
|
|
||||||
changingPreference = false
|
|
||||||
targetPreference = ""
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
console.warn("Failed to set network preference:", response.error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setConnectionPriority(type) {
|
|
||||||
if (type === "wifi") {
|
|
||||||
setNetworkPreference("wifi")
|
|
||||||
} else if (type === "ethernet") {
|
|
||||||
setNetworkPreference("ethernet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
|
||||||
connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch)
|
|
||||||
setNetworkPreference("wifi")
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleNetworkConnection(type) {
|
|
||||||
if (!networkAvailable) return
|
|
||||||
|
|
||||||
if (type === "ethernet") {
|
|
||||||
if (networkStatus === "ethernet") {
|
|
||||||
DMSService.sendRequest("network.ethernet.disconnect", null, null)
|
|
||||||
} else {
|
|
||||||
DMSService.sendRequest("network.ethernet.connect", null, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startAutoScan() {
|
|
||||||
autoScan = true
|
|
||||||
autoRefreshEnabled = true
|
|
||||||
if (networkAvailable && wifiEnabled) {
|
|
||||||
scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAutoScan() {
|
|
||||||
autoScan = false
|
|
||||||
autoRefreshEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchWiredNetworkInfo(uuid) {
|
|
||||||
if (!networkAvailable) return
|
|
||||||
|
|
||||||
networkWiredInfoUUID = uuid
|
|
||||||
networkWiredInfoLoading = true
|
|
||||||
networkWiredInfoDetails = "Loading network information..."
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.ethernet.info", { uuid: uuid }, response => {
|
|
||||||
networkWiredInfoLoading = false
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
networkWiredInfoDetails = "Failed to fetch network information"
|
|
||||||
} else if (response.result) {
|
|
||||||
formatWiredNetworkInfo(response.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatWiredNetworkInfo(info) {
|
|
||||||
let details = ""
|
|
||||||
|
|
||||||
if (!info) {
|
|
||||||
details = "Network information not found or network not available."
|
|
||||||
} else {
|
|
||||||
details += "Inteface: " + info.iface + "\\n"
|
|
||||||
details += "Driver: " + info.driver + "\\n"
|
|
||||||
details += "MAC Addr: " + info.hwAddr + "\\n"
|
|
||||||
details += "Speed: " + info.speed + " Mb/s\\n\\n"
|
|
||||||
|
|
||||||
details += "IPv4 informations:\\n"
|
|
||||||
|
|
||||||
for (const ip4 of info.IPv4s.ips) {
|
|
||||||
details += " IPv4 address: " + ip4 + "\\n"
|
|
||||||
}
|
|
||||||
details += " Gateway: " + info.IPv4s.gateway + "\\n"
|
|
||||||
details += " DNS: " + info.IPv4s.dns + "\\n"
|
|
||||||
|
|
||||||
if (info.IPv6s.ips) {
|
|
||||||
details += "\\nIPv6 informations:\\n"
|
|
||||||
|
|
||||||
for (const ip6 of info.IPv6s.ips) {
|
|
||||||
details += " IPv6 address: " + ip6 + "\\n"
|
|
||||||
}
|
|
||||||
if (info.IPv6s.gateway.length > 0) {
|
|
||||||
details += " Gateway: " + info.IPv6s.gateway + "\\n"
|
|
||||||
}
|
|
||||||
if (info.IPv6s.dns.length > 0) {
|
|
||||||
details += " DNS: " + info.IPv6s.dns + "\\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
networkWiredInfoDetails = details
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchNetworkInfo(ssid) {
|
|
||||||
if (!networkAvailable) return
|
|
||||||
|
|
||||||
networkInfoSSID = ssid
|
|
||||||
networkInfoLoading = true
|
|
||||||
networkInfoDetails = "Loading network information..."
|
|
||||||
|
|
||||||
DMSService.sendRequest("network.info", { ssid: ssid }, response => {
|
|
||||||
networkInfoLoading = false
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
networkInfoDetails = "Failed to fetch network information"
|
|
||||||
} else if (response.result) {
|
|
||||||
formatNetworkInfo(response.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNetworkInfo(info) {
|
|
||||||
let details = ""
|
|
||||||
|
|
||||||
if (!info || !info.bands || info.bands.length === 0) {
|
|
||||||
details = "Network information not found or network not available."
|
|
||||||
} else {
|
|
||||||
for (const band of info.bands) {
|
|
||||||
const freqGHz = band.frequency / 1000
|
|
||||||
let bandName = "Unknown"
|
|
||||||
if (band.frequency >= 2400 && band.frequency <= 2500) {
|
|
||||||
bandName = "2.4 GHz"
|
|
||||||
} else if (band.frequency >= 5000 && band.frequency <= 6000) {
|
|
||||||
bandName = "5 GHz"
|
|
||||||
} else if (band.frequency >= 6000) {
|
|
||||||
bandName = "6 GHz"
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusPrefix = band.connected ? "● " : " "
|
|
||||||
const statusSuffix = band.connected ? " (Connected)" : ""
|
|
||||||
|
|
||||||
details += statusPrefix + bandName + statusSuffix + " - " + band.signal + "%\\n"
|
|
||||||
details += " Channel " + band.channel + " (" + freqGHz.toFixed(1) + " GHz) • " + band.rate + " Mbit/s\\n"
|
|
||||||
details += " BSSID: " + band.bssid + "\\n"
|
|
||||||
details += " Mode: " + band.mode + "\\n"
|
|
||||||
details += " Security: " + (band.secured ? "Secured" : "Open") + "\\n"
|
|
||||||
if (band.saved) {
|
|
||||||
details += " Status: Saved network\\n"
|
|
||||||
}
|
|
||||||
details += "\\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
networkInfoDetails = details
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNetworkInfo(ssid) {
|
|
||||||
const network = wifiNetworks.find(n => n.ssid === ssid)
|
|
||||||
if (!network) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"ssid": network.ssid,
|
|
||||||
"signal": network.signal,
|
|
||||||
"secured": network.secured,
|
|
||||||
"saved": network.saved,
|
|
||||||
"connected": network.connected,
|
|
||||||
"bssid": network.bssid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWiredNetworkInfo(uuid) {
|
|
||||||
const network = wiredConnections.find(n => n.uuid === uuid)
|
|
||||||
if (!network) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"uuid": uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshNetworkState() {
|
|
||||||
if (networkAvailable) {
|
|
||||||
getState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitNmcliFields(line) {
|
|
||||||
const parts = []
|
|
||||||
let cur = ""
|
|
||||||
let escape = false
|
|
||||||
for (var i = 0; i < line.length; i++) {
|
|
||||||
const ch = line[i]
|
|
||||||
if (escape) {
|
|
||||||
cur += ch
|
|
||||||
escape = false
|
|
||||||
} else if (ch === '\\') {
|
|
||||||
escape = true
|
|
||||||
} else if (ch === ':') {
|
|
||||||
parts.push(cur)
|
|
||||||
cur = ""
|
|
||||||
} else {
|
|
||||||
cur += ch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parts.push(cur)
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,625 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Qt.labs.folderlistmodel
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var availablePlugins: ({})
|
|
||||||
property var loadedPlugins: ({})
|
|
||||||
property var pluginWidgetComponents: ({})
|
|
||||||
property var pluginDaemonComponents: ({})
|
|
||||||
property var pluginLauncherComponents: ({})
|
|
||||||
property string pluginDirectory: {
|
|
||||||
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
|
||||||
var configDirStr = configDir.toString()
|
|
||||||
if (configDirStr.startsWith("file://")) {
|
|
||||||
configDirStr = configDirStr.substring(7)
|
|
||||||
}
|
|
||||||
return configDirStr + "/DankMaterialShell/plugins"
|
|
||||||
}
|
|
||||||
property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins"
|
|
||||||
|
|
||||||
property var knownManifests: ({})
|
|
||||||
property var pathToPluginId: ({})
|
|
||||||
property var pluginInstances: ({})
|
|
||||||
property var globalVars: ({})
|
|
||||||
|
|
||||||
signal pluginLoaded(string pluginId)
|
|
||||||
signal pluginUnloaded(string pluginId)
|
|
||||||
signal pluginLoadFailed(string pluginId, string error)
|
|
||||||
signal pluginDataChanged(string pluginId)
|
|
||||||
signal pluginListUpdated()
|
|
||||||
signal globalVarChanged(string pluginId, string varName)
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: resyncDebounce
|
|
||||||
interval: 120
|
|
||||||
repeat: false
|
|
||||||
onTriggered: resyncAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
userWatcher.folder = Paths.toFileUrl(root.pluginDirectory)
|
|
||||||
systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory)
|
|
||||||
Qt.callLater(resyncAll)
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderListModel {
|
|
||||||
id: userWatcher
|
|
||||||
showDirs: true
|
|
||||||
showFiles: false
|
|
||||||
showDotAndDotDot: false
|
|
||||||
nameFilters: ["plugin.json"]
|
|
||||||
|
|
||||||
onCountChanged: resyncDebounce.restart()
|
|
||||||
onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderListModel {
|
|
||||||
id: systemWatcher
|
|
||||||
showDirs: true
|
|
||||||
showFiles: false
|
|
||||||
showDotAndDotDot: false
|
|
||||||
nameFilters: ["plugin.json"]
|
|
||||||
|
|
||||||
onCountChanged: resyncDebounce.restart()
|
|
||||||
onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapshotModel(model, sourceTag) {
|
|
||||||
const out = []
|
|
||||||
const n = model.count
|
|
||||||
const baseDir = sourceTag === "user" ? pluginDirectory : systemPluginDirectory
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
let dirPath = model.get(i, "filePath")
|
|
||||||
if (dirPath.startsWith("file://")) {
|
|
||||||
dirPath = dirPath.substring(7)
|
|
||||||
}
|
|
||||||
if (!dirPath.startsWith(baseDir)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const manifestPath = dirPath + "/plugin.json"
|
|
||||||
out.push({ path: manifestPath, source: sourceTag })
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function resyncAll() {
|
|
||||||
const userList = snapshotModel(userWatcher, "user")
|
|
||||||
const sysList = snapshotModel(systemWatcher, "system")
|
|
||||||
const seenPaths = {}
|
|
||||||
|
|
||||||
function consider(entry) {
|
|
||||||
const key = entry.path
|
|
||||||
seenPaths[key] = true
|
|
||||||
const prev = knownManifests[key]
|
|
||||||
if (!prev) {
|
|
||||||
loadPluginManifestFile(entry.path, entry.source, Date.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i=0;i<userList.length;i++) consider(userList[i])
|
|
||||||
for (let i=0;i<sysList.length;i++) consider(sysList[i])
|
|
||||||
|
|
||||||
const removed = []
|
|
||||||
for (const path in knownManifests) {
|
|
||||||
if (!seenPaths[path]) removed.push(path)
|
|
||||||
}
|
|
||||||
if (removed.length) {
|
|
||||||
removed.forEach(function(path) {
|
|
||||||
const pid = pathToPluginId[path]
|
|
||||||
if (pid) {
|
|
||||||
unregisterPluginByPath(path, pid)
|
|
||||||
}
|
|
||||||
delete knownManifests[path]
|
|
||||||
delete pathToPluginId[path]
|
|
||||||
})
|
|
||||||
pluginListUpdated()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPluginManifestFile(manifestPathNoScheme, sourceTag, mtimeEpochMs) {
|
|
||||||
const manifestId = "m_" + Math.random().toString(36).slice(2)
|
|
||||||
const qml = `
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell.Io
|
|
||||||
FileView {
|
|
||||||
id: fv
|
|
||||||
property string absPath: ""
|
|
||||||
onLoaded: {
|
|
||||||
try {
|
|
||||||
let raw = text()
|
|
||||||
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1)
|
|
||||||
const manifest = JSON.parse(raw)
|
|
||||||
root._onManifestParsed(absPath, manifest, "${sourceTag}", ${mtimeEpochMs})
|
|
||||||
} catch (e) {
|
|
||||||
console.error("PluginService: bad manifest", absPath, e.message)
|
|
||||||
knownManifests[absPath] = { mtime: ${mtimeEpochMs}, source: "${sourceTag}", bad: true }
|
|
||||||
}
|
|
||||||
fv.destroy()
|
|
||||||
}
|
|
||||||
onLoadFailed: (err) => {
|
|
||||||
console.warn("PluginService: manifest load failed", absPath, err)
|
|
||||||
fv.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const loader = Qt.createQmlObject(qml, root, "mf_" + manifestId)
|
|
||||||
loader.absPath = manifestPathNoScheme
|
|
||||||
loader.path = manifestPathNoScheme
|
|
||||||
}
|
|
||||||
|
|
||||||
function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) {
|
|
||||||
if (!manifest || !manifest.id || !manifest.name || !manifest.component) {
|
|
||||||
console.error("PluginService: invalid manifest fields:", absPath)
|
|
||||||
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, bad: true }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const dir = absPath.substring(0, absPath.lastIndexOf('/'))
|
|
||||||
let comp = manifest.component
|
|
||||||
if (comp.startsWith("./")) comp = comp.slice(2)
|
|
||||||
let settings = manifest.settings
|
|
||||||
if (settings && settings.startsWith("./")) settings = settings.slice(2)
|
|
||||||
|
|
||||||
const info = {}
|
|
||||||
for (const k in manifest) info[k] = manifest[k]
|
|
||||||
|
|
||||||
let perms = manifest.permissions
|
|
||||||
if (typeof perms === "string") {
|
|
||||||
perms = perms.split(/\s*,\s*/)
|
|
||||||
}
|
|
||||||
if (!Array.isArray(perms)) {
|
|
||||||
perms = []
|
|
||||||
}
|
|
||||||
info.permissions = perms.map(p => String(p).trim())
|
|
||||||
|
|
||||||
info.manifestPath = absPath
|
|
||||||
info.pluginDirectory = dir
|
|
||||||
info.componentPath = dir + "/" + comp
|
|
||||||
info.settingsPath = settings ? (dir + "/" + settings) : null
|
|
||||||
info.loaded = isPluginLoaded(manifest.id)
|
|
||||||
info.type = manifest.type || "widget"
|
|
||||||
info.source = sourceTag
|
|
||||||
|
|
||||||
const existing = availablePlugins[manifest.id]
|
|
||||||
const shouldReplace =
|
|
||||||
(!existing) ||
|
|
||||||
(existing && existing.source === "system" && sourceTag === "user")
|
|
||||||
|
|
||||||
if (shouldReplace) {
|
|
||||||
if (existing && existing.loaded && existing.source !== sourceTag) {
|
|
||||||
unloadPlugin(manifest.id)
|
|
||||||
}
|
|
||||||
const newMap = Object.assign({}, availablePlugins)
|
|
||||||
newMap[manifest.id] = info
|
|
||||||
availablePlugins = newMap
|
|
||||||
pathToPluginId[absPath] = manifest.id
|
|
||||||
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag }
|
|
||||||
pluginListUpdated()
|
|
||||||
const enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false)
|
|
||||||
if (enabled && !info.loaded) loadPlugin(manifest.id)
|
|
||||||
} else {
|
|
||||||
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, shadowedBy: existing.source }
|
|
||||||
pathToPluginId[absPath] = manifest.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unregisterPluginByPath(absPath, pluginId) {
|
|
||||||
const current = availablePlugins[pluginId]
|
|
||||||
if (current && current.manifestPath === absPath) {
|
|
||||||
if (current.loaded) unloadPlugin(pluginId)
|
|
||||||
const newMap = Object.assign({}, availablePlugins)
|
|
||||||
delete newMap[pluginId]
|
|
||||||
availablePlugins = newMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPlugin(pluginId) {
|
|
||||||
const plugin = availablePlugins[pluginId]
|
|
||||||
if (!plugin) {
|
|
||||||
console.error("PluginService: Plugin not found:", pluginId)
|
|
||||||
pluginLoadFailed(pluginId, "Plugin not found")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.loaded) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDaemon = plugin.type === "daemon"
|
|
||||||
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"))
|
|
||||||
const map = isDaemon ? pluginDaemonComponents : isLauncher ? pluginLauncherComponents : pluginWidgetComponents
|
|
||||||
|
|
||||||
const prevInstance = pluginInstances[pluginId]
|
|
||||||
if (prevInstance) {
|
|
||||||
prevInstance.destroy()
|
|
||||||
const newInstances = Object.assign({}, pluginInstances)
|
|
||||||
delete newInstances[pluginId]
|
|
||||||
pluginInstances = newInstances
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = "file://" + plugin.componentPath
|
|
||||||
const comp = Qt.createComponent(url, Component.PreferSynchronous)
|
|
||||||
if (comp.status === Component.Error) {
|
|
||||||
console.error("PluginService: component error", pluginId, comp.errorString())
|
|
||||||
pluginLoadFailed(pluginId, comp.errorString())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDaemon) {
|
|
||||||
const instance = comp.createObject(root, { "pluginId": pluginId })
|
|
||||||
if (!instance) {
|
|
||||||
console.error("PluginService: failed to instantiate daemon:", pluginId, comp.errorString())
|
|
||||||
pluginLoadFailed(pluginId, comp.errorString())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const newInstances = Object.assign({}, pluginInstances)
|
|
||||||
newInstances[pluginId] = instance
|
|
||||||
pluginInstances = newInstances
|
|
||||||
|
|
||||||
const newDaemons = Object.assign({}, pluginDaemonComponents)
|
|
||||||
newDaemons[pluginId] = comp
|
|
||||||
pluginDaemonComponents = newDaemons
|
|
||||||
} else if (isLauncher) {
|
|
||||||
const newLaunchers = Object.assign({}, pluginLauncherComponents)
|
|
||||||
newLaunchers[pluginId] = comp
|
|
||||||
pluginLauncherComponents = newLaunchers
|
|
||||||
} else {
|
|
||||||
const newComponents = Object.assign({}, pluginWidgetComponents)
|
|
||||||
newComponents[pluginId] = comp
|
|
||||||
pluginWidgetComponents = newComponents
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.loaded = true
|
|
||||||
loadedPlugins[pluginId] = plugin
|
|
||||||
|
|
||||||
pluginLoaded(pluginId)
|
|
||||||
return true
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("PluginService: Error loading plugin:", pluginId, e.message)
|
|
||||||
pluginLoadFailed(pluginId, e.message)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unloadPlugin(pluginId) {
|
|
||||||
const plugin = loadedPlugins[pluginId]
|
|
||||||
if (!plugin) {
|
|
||||||
console.warn("PluginService: Plugin not loaded:", pluginId)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isDaemon = plugin.type === "daemon"
|
|
||||||
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"))
|
|
||||||
|
|
||||||
const instance = pluginInstances[pluginId]
|
|
||||||
if (instance) {
|
|
||||||
instance.destroy()
|
|
||||||
const newInstances = Object.assign({}, pluginInstances)
|
|
||||||
delete newInstances[pluginId]
|
|
||||||
pluginInstances = newInstances
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDaemon && pluginDaemonComponents[pluginId]) {
|
|
||||||
const newDaemons = Object.assign({}, pluginDaemonComponents)
|
|
||||||
delete newDaemons[pluginId]
|
|
||||||
pluginDaemonComponents = newDaemons
|
|
||||||
} else if (isLauncher && pluginLauncherComponents[pluginId]) {
|
|
||||||
const newLaunchers = Object.assign({}, pluginLauncherComponents)
|
|
||||||
delete newLaunchers[pluginId]
|
|
||||||
pluginLauncherComponents = newLaunchers
|
|
||||||
} else if (pluginWidgetComponents[pluginId]) {
|
|
||||||
const newComponents = Object.assign({}, pluginWidgetComponents)
|
|
||||||
delete newComponents[pluginId]
|
|
||||||
pluginWidgetComponents = newComponents
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.loaded = false
|
|
||||||
delete loadedPlugins[pluginId]
|
|
||||||
|
|
||||||
pluginUnloaded(pluginId)
|
|
||||||
return true
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("PluginService: Error unloading plugin:", pluginId, "Error:", error.message)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWidgetComponents() {
|
|
||||||
return pluginWidgetComponents
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDaemonComponents() {
|
|
||||||
return pluginDaemonComponents
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAvailablePlugins() {
|
|
||||||
const result = []
|
|
||||||
for (const key in availablePlugins) {
|
|
||||||
result.push(availablePlugins[key])
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPluginVariants(pluginId) {
|
|
||||||
const plugin = availablePlugins[pluginId]
|
|
||||||
if (!plugin) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const variants = SettingsData.getPluginSetting(pluginId, "variants", [])
|
|
||||||
return variants
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllPluginVariants() {
|
|
||||||
const result = []
|
|
||||||
for (const pluginId in availablePlugins) {
|
|
||||||
const plugin = availablePlugins[pluginId]
|
|
||||||
if (plugin.type !== "widget") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const variants = getPluginVariants(pluginId)
|
|
||||||
if (variants.length === 0) {
|
|
||||||
result.push({
|
|
||||||
pluginId: pluginId,
|
|
||||||
variantId: null,
|
|
||||||
fullId: pluginId,
|
|
||||||
name: plugin.name,
|
|
||||||
icon: plugin.icon || "extension",
|
|
||||||
description: plugin.description || "Plugin widget",
|
|
||||||
loaded: plugin.loaded
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < variants.length; i++) {
|
|
||||||
const variant = variants[i]
|
|
||||||
result.push({
|
|
||||||
pluginId: pluginId,
|
|
||||||
variantId: variant.id,
|
|
||||||
fullId: pluginId + ":" + variant.id,
|
|
||||||
name: plugin.name + " - " + variant.name,
|
|
||||||
icon: variant.icon || plugin.icon || "extension",
|
|
||||||
description: variant.description || plugin.description || "Plugin widget variant",
|
|
||||||
loaded: plugin.loaded
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPluginVariant(pluginId, variantName, variantConfig) {
|
|
||||||
const variants = getPluginVariants(pluginId)
|
|
||||||
const variantId = "variant_" + Date.now()
|
|
||||||
const newVariant = Object.assign({}, variantConfig, {
|
|
||||||
id: variantId,
|
|
||||||
name: variantName
|
|
||||||
})
|
|
||||||
variants.push(newVariant)
|
|
||||||
SettingsData.setPluginSetting(pluginId, "variants", variants)
|
|
||||||
pluginDataChanged(pluginId)
|
|
||||||
return variantId
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePluginVariant(pluginId, variantId) {
|
|
||||||
const variants = getPluginVariants(pluginId)
|
|
||||||
const newVariants = variants.filter(function(v) { return v.id !== variantId })
|
|
||||||
SettingsData.setPluginSetting(pluginId, "variants", newVariants)
|
|
||||||
|
|
||||||
const fullId = pluginId + ":" + variantId
|
|
||||||
removeWidgetFromDankBar(fullId)
|
|
||||||
|
|
||||||
pluginDataChanged(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWidgetFromDankBar(widgetId) {
|
|
||||||
function filterWidget(widget) {
|
|
||||||
const id = typeof widget === "string" ? widget : widget.id
|
|
||||||
return id !== widgetId
|
|
||||||
}
|
|
||||||
|
|
||||||
const leftWidgets = SettingsData.dankBarLeftWidgets
|
|
||||||
const centerWidgets = SettingsData.dankBarCenterWidgets
|
|
||||||
const rightWidgets = SettingsData.dankBarRightWidgets
|
|
||||||
|
|
||||||
const newLeft = leftWidgets.filter(filterWidget)
|
|
||||||
const newCenter = centerWidgets.filter(filterWidget)
|
|
||||||
const newRight = rightWidgets.filter(filterWidget)
|
|
||||||
|
|
||||||
if (newLeft.length !== leftWidgets.length) {
|
|
||||||
SettingsData.setDankBarLeftWidgets(newLeft)
|
|
||||||
}
|
|
||||||
if (newCenter.length !== centerWidgets.length) {
|
|
||||||
SettingsData.setDankBarCenterWidgets(newCenter)
|
|
||||||
}
|
|
||||||
if (newRight.length !== rightWidgets.length) {
|
|
||||||
SettingsData.setDankBarRightWidgets(newRight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePluginVariant(pluginId, variantId, variantConfig) {
|
|
||||||
const variants = getPluginVariants(pluginId)
|
|
||||||
for (let i = 0; i < variants.length; i++) {
|
|
||||||
if (variants[i].id === variantId) {
|
|
||||||
variants[i] = Object.assign({}, variants[i], variantConfig)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SettingsData.setPluginSetting(pluginId, "variants", variants)
|
|
||||||
pluginDataChanged(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPluginVariantData(pluginId, variantId) {
|
|
||||||
const variants = getPluginVariants(pluginId)
|
|
||||||
for (let i = 0; i < variants.length; i++) {
|
|
||||||
if (variants[i].id === variantId) {
|
|
||||||
return variants[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLoadedPlugins() {
|
|
||||||
const result = []
|
|
||||||
for (const key in loadedPlugins) {
|
|
||||||
result.push(loadedPlugins[key])
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPluginLoaded(pluginId) {
|
|
||||||
return loadedPlugins[pluginId] !== undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function enablePlugin(pluginId) {
|
|
||||||
SettingsData.setPluginSetting(pluginId, "enabled", true)
|
|
||||||
return loadPlugin(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function disablePlugin(pluginId) {
|
|
||||||
SettingsData.setPluginSetting(pluginId, "enabled", false)
|
|
||||||
return unloadPlugin(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadPlugin(pluginId) {
|
|
||||||
if (isPluginLoaded(pluginId)) {
|
|
||||||
unloadPlugin(pluginId)
|
|
||||||
}
|
|
||||||
return loadPlugin(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePluginData(pluginId, key, value) {
|
|
||||||
SettingsData.setPluginSetting(pluginId, key, value)
|
|
||||||
pluginDataChanged(pluginId)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPluginData(pluginId, key, defaultValue) {
|
|
||||||
return SettingsData.getPluginSetting(pluginId, key, defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveAllPluginSettings() {
|
|
||||||
SettingsData.savePluginSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanPlugins() {
|
|
||||||
resyncDebounce.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function forceRescanPlugin(pluginId) {
|
|
||||||
const plugin = availablePlugins[pluginId]
|
|
||||||
if (plugin && plugin.manifestPath) {
|
|
||||||
const manifestPath = plugin.manifestPath
|
|
||||||
const source = plugin.source || "user"
|
|
||||||
delete knownManifests[manifestPath]
|
|
||||||
const newMap = Object.assign({}, availablePlugins)
|
|
||||||
delete newMap[pluginId]
|
|
||||||
availablePlugins = newMap
|
|
||||||
loadPluginManifestFile(manifestPath, source, Date.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPluginDirectory() {
|
|
||||||
const mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }")
|
|
||||||
if (mkdirProcess.status === Component.Ready) {
|
|
||||||
const process = mkdirProcess.createObject(root)
|
|
||||||
process.command = ["mkdir", "-p", pluginDirectory]
|
|
||||||
process.exited.connect(function(exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.error("PluginService: Failed to create plugin directory, exit code:", exitCode)
|
|
||||||
}
|
|
||||||
process.destroy()
|
|
||||||
})
|
|
||||||
process.running = true
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
console.error("PluginService: Failed to create mkdir process")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launcher plugin helper functions
|
|
||||||
function getLauncherPlugins() {
|
|
||||||
const launchers = {}
|
|
||||||
|
|
||||||
// Check plugins that have launcher components
|
|
||||||
for (const pluginId in pluginLauncherComponents) {
|
|
||||||
const plugin = availablePlugins[pluginId]
|
|
||||||
if (plugin && plugin.loaded) {
|
|
||||||
launchers[pluginId] = plugin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return launchers
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLauncherPlugin(pluginId) {
|
|
||||||
const plugin = availablePlugins[pluginId]
|
|
||||||
if (plugin && plugin.loaded && pluginLauncherComponents[pluginId]) {
|
|
||||||
return plugin
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPluginTrigger(pluginId) {
|
|
||||||
const plugin = getLauncherPlugin(pluginId)
|
|
||||||
if (plugin) {
|
|
||||||
const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!")
|
|
||||||
return customTrigger
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllPluginTriggers() {
|
|
||||||
const triggers = {}
|
|
||||||
const launchers = getLauncherPlugins()
|
|
||||||
|
|
||||||
for (const pluginId in launchers) {
|
|
||||||
const trigger = getPluginTrigger(pluginId)
|
|
||||||
if (trigger && trigger.trim() !== "") {
|
|
||||||
triggers[trigger] = pluginId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return triggers
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPluginsWithEmptyTrigger() {
|
|
||||||
const plugins = []
|
|
||||||
const launchers = getLauncherPlugins()
|
|
||||||
|
|
||||||
for (const pluginId in launchers) {
|
|
||||||
const trigger = getPluginTrigger(pluginId)
|
|
||||||
if (!trigger || trigger.trim() === "") {
|
|
||||||
plugins.push(pluginId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return plugins
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGlobalVar(pluginId, varName, defaultValue) {
|
|
||||||
if (globalVars[pluginId] && varName in globalVars[pluginId]) {
|
|
||||||
return globalVars[pluginId][varName]
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGlobalVar(pluginId, varName, value) {
|
|
||||||
const newGlobals = Object.assign({}, globalVars)
|
|
||||||
if (!newGlobals[pluginId]) {
|
|
||||||
newGlobals[pluginId] = {}
|
|
||||||
}
|
|
||||||
newGlobals[pluginId] = Object.assign({}, newGlobals[pluginId])
|
|
||||||
newGlobals[pluginId][varName] = value
|
|
||||||
globalVars = newGlobals
|
|
||||||
globalVarChanged(pluginId, varName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +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 string currentCommand: ""
|
|
||||||
property bool hasDetails: false
|
|
||||||
property string wallpaperErrorStatus: ""
|
|
||||||
|
|
||||||
function showToast(message, level = levelInfo, details = "", command = "") {
|
|
||||||
toastQueue.push({
|
|
||||||
"message": message,
|
|
||||||
"level": level,
|
|
||||||
"details": details,
|
|
||||||
"command": command
|
|
||||||
})
|
|
||||||
if (!toastVisible) {
|
|
||||||
processQueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showInfo(message, details = "", command = "") {
|
|
||||||
showToast(message, levelInfo, details, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWarning(message, details = "", command = "") {
|
|
||||||
showToast(message, levelWarn, details, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message, details = "", command = "") {
|
|
||||||
showToast(message, levelError, details, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideToast() {
|
|
||||||
toastVisible = false
|
|
||||||
currentMessage = ""
|
|
||||||
currentDetails = ""
|
|
||||||
currentCommand = ""
|
|
||||||
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 || ""
|
|
||||||
currentCommand = toast.command || ""
|
|
||||||
hasDetails = currentDetails.length > 0 || currentCommand.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,88 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
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() {
|
|
||||||
Proc.runCommand("userInfo", ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""], (output, exitCode) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.username = "User"
|
|
||||||
root.fullName = "User"
|
|
||||||
root.hostname = "System"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const parts = output.trim().split("|")
|
|
||||||
if (parts.length >= 3) {
|
|
||||||
root.username = parts[0] || ""
|
|
||||||
root.fullName = parts[1] || parts[0] || ""
|
|
||||||
root.hostname = parts[2] || ""
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUptime() {
|
|
||||||
Proc.runCommand("uptime", ["cat", "/proc/uptime"], (output, exitCode) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.uptime = "Unknown"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const seconds = parseInt(output.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`
|
|
||||||
}
|
|
||||||
|
|
||||||
let shortUptime = "up"
|
|
||||||
if (days > 0) {
|
|
||||||
shortUptime += ` ${days}d`
|
|
||||||
}
|
|
||||||
if (hours > 0) {
|
|
||||||
shortUptime += ` ${hours}h`
|
|
||||||
}
|
|
||||||
if (minutes > 0) {
|
|
||||||
shortUptime += ` ${minutes}m`
|
|
||||||
}
|
|
||||||
root.shortUptime = shortUptime
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshUserInfo() {
|
|
||||||
getUserInfo()
|
|
||||||
getUptime()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
getUserInfo()
|
|
||||||
getUptime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +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
|
|
||||||
|
|
||||||
property int refCount: 0
|
|
||||||
|
|
||||||
onRefCountChanged: {
|
|
||||||
console.log("VpnService: refCount changed to", refCount)
|
|
||||||
if (refCount > 0 && !nmMonitor.running) {
|
|
||||||
console.log("VpnService: Starting nmMonitor")
|
|
||||||
nmMonitor.running = true
|
|
||||||
refreshAll()
|
|
||||||
} else if (refCount === 0 && nmMonitor.running) {
|
|
||||||
console.log("VpnService: Stopping nmMonitor")
|
|
||||||
nmMonitor.running = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.)
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,657 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int refCount: 0
|
|
||||||
|
|
||||||
property var weather: ({
|
|
||||||
"available": false,
|
|
||||||
"loading": true,
|
|
||||||
"temp": 0,
|
|
||||||
"tempF": 0,
|
|
||||||
"feelsLike": 0,
|
|
||||||
"feelsLikeF": 0,
|
|
||||||
"city": "",
|
|
||||||
"country": "",
|
|
||||||
"wCode": 0,
|
|
||||||
"humidity": 0,
|
|
||||||
"wind": "",
|
|
||||||
"sunrise": "06:00",
|
|
||||||
"sunset": "18:00",
|
|
||||||
"uv": 0,
|
|
||||||
"pressure": 0,
|
|
||||||
"precipitationProbability": 0,
|
|
||||||
"isDay": true,
|
|
||||||
"forecast": []
|
|
||||||
})
|
|
||||||
|
|
||||||
property var location: null
|
|
||||||
property int updateInterval: 900000 // 15 minutes
|
|
||||||
property int retryAttempts: 0
|
|
||||||
property int maxRetryAttempts: 3
|
|
||||||
property int retryDelay: 30000
|
|
||||||
property int lastFetchTime: 0
|
|
||||||
property int minFetchInterval: 30000
|
|
||||||
property int persistentRetryCount: 0
|
|
||||||
|
|
||||||
readonly property var lowPriorityCmd: ["nice", "-n", "19", "ionice", "-c3"]
|
|
||||||
readonly property var curlBaseCmd: ["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "100k", "--compressed"]
|
|
||||||
|
|
||||||
property var weatherIcons: ({
|
|
||||||
"0": "clear_day",
|
|
||||||
"1": "clear_day",
|
|
||||||
"2": "partly_cloudy_day",
|
|
||||||
"3": "cloud",
|
|
||||||
"45": "foggy",
|
|
||||||
"48": "foggy",
|
|
||||||
"51": "rainy",
|
|
||||||
"53": "rainy",
|
|
||||||
"55": "rainy",
|
|
||||||
"56": "rainy",
|
|
||||||
"57": "rainy",
|
|
||||||
"61": "rainy",
|
|
||||||
"63": "rainy",
|
|
||||||
"65": "rainy",
|
|
||||||
"66": "rainy",
|
|
||||||
"67": "rainy",
|
|
||||||
"71": "cloudy_snowing",
|
|
||||||
"73": "cloudy_snowing",
|
|
||||||
"75": "snowing_heavy",
|
|
||||||
"77": "cloudy_snowing",
|
|
||||||
"80": "rainy",
|
|
||||||
"81": "rainy",
|
|
||||||
"82": "rainy",
|
|
||||||
"85": "cloudy_snowing",
|
|
||||||
"86": "snowing_heavy",
|
|
||||||
"95": "thunderstorm",
|
|
||||||
"96": "thunderstorm",
|
|
||||||
"99": "thunderstorm"
|
|
||||||
})
|
|
||||||
|
|
||||||
property var nightWeatherIcons: ({
|
|
||||||
"0": "clear_night",
|
|
||||||
"1": "clear_night",
|
|
||||||
"2": "partly_cloudy_night",
|
|
||||||
"3": "cloud",
|
|
||||||
"45": "foggy",
|
|
||||||
"48": "foggy",
|
|
||||||
"51": "rainy",
|
|
||||||
"53": "rainy",
|
|
||||||
"55": "rainy",
|
|
||||||
"56": "rainy",
|
|
||||||
"57": "rainy",
|
|
||||||
"61": "rainy",
|
|
||||||
"63": "rainy",
|
|
||||||
"65": "rainy",
|
|
||||||
"66": "rainy",
|
|
||||||
"67": "rainy",
|
|
||||||
"71": "cloudy_snowing",
|
|
||||||
"73": "cloudy_snowing",
|
|
||||||
"75": "snowing_heavy",
|
|
||||||
"77": "cloudy_snowing",
|
|
||||||
"80": "rainy",
|
|
||||||
"81": "rainy",
|
|
||||||
"82": "rainy",
|
|
||||||
"85": "cloudy_snowing",
|
|
||||||
"86": "snowing_heavy",
|
|
||||||
"95": "thunderstorm",
|
|
||||||
"96": "thunderstorm",
|
|
||||||
"99": "thunderstorm"
|
|
||||||
})
|
|
||||||
|
|
||||||
function getWeatherIcon(code, isDay) {
|
|
||||||
if (typeof isDay === "undefined") {
|
|
||||||
isDay = weather.isDay
|
|
||||||
}
|
|
||||||
const iconMap = isDay ? weatherIcons : nightWeatherIcons
|
|
||||||
return iconMap[String(code)] || "cloud"
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWeatherCondition(code) {
|
|
||||||
const conditions = {
|
|
||||||
"0": "Clear",
|
|
||||||
"1": "Clear",
|
|
||||||
"2": "Partly cloudy",
|
|
||||||
"3": "Overcast",
|
|
||||||
"45": "Fog",
|
|
||||||
"48": "Fog",
|
|
||||||
"51": "Drizzle",
|
|
||||||
"53": "Drizzle",
|
|
||||||
"55": "Drizzle",
|
|
||||||
"56": "Freezing drizzle",
|
|
||||||
"57": "Freezing drizzle",
|
|
||||||
"61": "Light rain",
|
|
||||||
"63": "Rain",
|
|
||||||
"65": "Heavy rain",
|
|
||||||
"66": "Light rain",
|
|
||||||
"67": "Heavy rain",
|
|
||||||
"71": "Light snow",
|
|
||||||
"73": "Snow",
|
|
||||||
"75": "Heavy snow",
|
|
||||||
"77": "Snow",
|
|
||||||
"80": "Light rain",
|
|
||||||
"81": "Rain",
|
|
||||||
"82": "Heavy rain",
|
|
||||||
"85": "Light snow showers",
|
|
||||||
"86": "Heavy snow showers",
|
|
||||||
"95": "Thunderstorm",
|
|
||||||
"96": "Thunderstorm with hail",
|
|
||||||
"99": "Thunderstorm with hail"
|
|
||||||
}
|
|
||||||
return conditions[String(code)] || "Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(isoString) {
|
|
||||||
if (!isoString) return "--"
|
|
||||||
|
|
||||||
try {
|
|
||||||
const date = new Date(isoString)
|
|
||||||
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
|
||||||
return date.toLocaleTimeString(Qt.locale(), format)
|
|
||||||
} catch (e) {
|
|
||||||
return "--"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatForecastDay(isoString, index) {
|
|
||||||
if (!isoString) return "--"
|
|
||||||
|
|
||||||
try {
|
|
||||||
const date = new Date(isoString)
|
|
||||||
if (index === 0) return I18n.tr("Today")
|
|
||||||
if (index === 1) return I18n.tr("Tomorrow")
|
|
||||||
|
|
||||||
const locale = Qt.locale()
|
|
||||||
return locale.dayName(date.getDay(), Locale.ShortFormat)
|
|
||||||
} catch (e) {
|
|
||||||
return "--"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWeatherApiUrl() {
|
|
||||||
if (!location) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = [
|
|
||||||
"latitude=" + location.latitude,
|
|
||||||
"longitude=" + location.longitude,
|
|
||||||
"current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m",
|
|
||||||
"daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max",
|
|
||||||
"timezone=auto",
|
|
||||||
"forecast_days=7"
|
|
||||||
]
|
|
||||||
|
|
||||||
if (SettingsData.useFahrenheit) {
|
|
||||||
params.push("temperature_unit=fahrenheit")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "https://api.open-meteo.com/v1/forecast?" + params.join('&')
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGeocodingUrl(query) {
|
|
||||||
return "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(query) + "&count=1&language=en&format=json"
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRef() {
|
|
||||||
refCount++
|
|
||||||
|
|
||||||
if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) {
|
|
||||||
fetchWeather()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeRef() {
|
|
||||||
refCount = Math.max(0, refCount - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLocation() {
|
|
||||||
if (SettingsData.useAutoLocation) {
|
|
||||||
getLocationFromIP()
|
|
||||||
} else {
|
|
||||||
const coords = SettingsData.weatherCoordinates
|
|
||||||
if (coords) {
|
|
||||||
const parts = coords.split(",")
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const lat = parseFloat(parts[0])
|
|
||||||
const lon = parseFloat(parts[1])
|
|
||||||
if (!isNaN(lat) && !isNaN(lon)) {
|
|
||||||
getLocationFromCoords(lat, lon)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cityName = SettingsData.weatherLocation
|
|
||||||
if (cityName) {
|
|
||||||
getLocationFromCity(cityName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocationFromCoords(lat, lon) {
|
|
||||||
const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en"
|
|
||||||
reverseGeocodeFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat(["-H", "User-Agent: DankMaterialShell Weather Widget", url])
|
|
||||||
reverseGeocodeFetcher.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocationFromCity(city) {
|
|
||||||
cityGeocodeFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([getGeocodingUrl(city)])
|
|
||||||
cityGeocodeFetcher.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocationFromIP() {
|
|
||||||
ipLocationFetcher.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchWeather() {
|
|
||||||
if (root.refCount === 0 || !SettingsData.weatherEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!location) {
|
|
||||||
updateLocation()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weatherFetcher.running) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now()
|
|
||||||
if (now - root.lastFetchTime < root.minFetchInterval) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiUrl = getWeatherApiUrl()
|
|
||||||
if (!apiUrl) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root.lastFetchTime = now
|
|
||||||
root.weather.loading = true
|
|
||||||
const weatherCmd = lowPriorityCmd.concat(["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "150k", "--compressed"])
|
|
||||||
weatherFetcher.command = weatherCmd.concat([apiUrl])
|
|
||||||
weatherFetcher.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function forceRefresh() {
|
|
||||||
root.lastFetchTime = 0 // Reset throttle
|
|
||||||
fetchWeather()
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextInterval() {
|
|
||||||
const jitter = Math.floor(Math.random() * 15000) - 7500
|
|
||||||
return Math.max(60000, root.updateInterval + jitter)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWeatherSuccess() {
|
|
||||||
root.retryAttempts = 0
|
|
||||||
root.persistentRetryCount = 0
|
|
||||||
if (persistentRetryTimer.running) {
|
|
||||||
persistentRetryTimer.stop()
|
|
||||||
}
|
|
||||||
if (updateTimer.interval !== root.updateInterval) {
|
|
||||||
updateTimer.interval = root.updateInterval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWeatherFailure() {
|
|
||||||
root.retryAttempts++
|
|
||||||
if (root.retryAttempts < root.maxRetryAttempts) {
|
|
||||||
retryTimer.start()
|
|
||||||
} else {
|
|
||||||
root.retryAttempts = 0
|
|
||||||
if (!root.weather.available) {
|
|
||||||
root.weather.loading = false
|
|
||||||
}
|
|
||||||
const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000)
|
|
||||||
persistentRetryCount++
|
|
||||||
persistentRetryTimer.interval = backoffDelay
|
|
||||||
persistentRetryTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ipLocationFetcher
|
|
||||||
command: lowPriorityCmd.concat(curlBaseCmd).concat(["http://ipinfo.io/json"])
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const raw = text.trim()
|
|
||||||
if (!raw || raw[0] !== "{") {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(raw)
|
|
||||||
const coords = data.loc
|
|
||||||
const city = data.city
|
|
||||||
|
|
||||||
if (!coords || !city) {
|
|
||||||
throw new Error("Missing location data")
|
|
||||||
}
|
|
||||||
|
|
||||||
const coordsParts = coords.split(",")
|
|
||||||
if (coordsParts.length !== 2) {
|
|
||||||
throw new Error("Invalid coordinates format")
|
|
||||||
}
|
|
||||||
|
|
||||||
const lat = parseFloat(coordsParts[0])
|
|
||||||
const lon = parseFloat(coordsParts[1])
|
|
||||||
|
|
||||||
if (isNaN(lat) || isNaN(lon)) {
|
|
||||||
throw new Error("Invalid coordinate values")
|
|
||||||
}
|
|
||||||
|
|
||||||
root.location = {
|
|
||||||
city: city,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon
|
|
||||||
}
|
|
||||||
fetchWeather()
|
|
||||||
} catch (e) {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: reverseGeocodeFetcher
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const raw = text.trim()
|
|
||||||
if (!raw || raw[0] !== "{") {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(raw)
|
|
||||||
const address = data.address || {}
|
|
||||||
|
|
||||||
root.location = {
|
|
||||||
city: address.hamlet || address.city || address.town || address.village || "Unknown",
|
|
||||||
country: address.country || "Unknown",
|
|
||||||
latitude: parseFloat(data.lat),
|
|
||||||
longitude: parseFloat(data.lon)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchWeather()
|
|
||||||
} catch (e) {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: cityGeocodeFetcher
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const raw = text.trim()
|
|
||||||
if (!raw || raw[0] !== "{") {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(raw)
|
|
||||||
const results = data.results
|
|
||||||
|
|
||||||
if (!results || results.length === 0) {
|
|
||||||
throw new Error("No results found")
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = results[0]
|
|
||||||
|
|
||||||
root.location = {
|
|
||||||
city: result.name,
|
|
||||||
country: result.country,
|
|
||||||
latitude: result.latitude,
|
|
||||||
longitude: result.longitude
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchWeather()
|
|
||||||
} catch (e) {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: weatherFetcher
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const raw = text.trim()
|
|
||||||
if (!raw || raw[0] !== "{") {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(raw)
|
|
||||||
|
|
||||||
if (!data.current || !data.daily) {
|
|
||||||
throw new Error("Required weather data fields missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = data.current
|
|
||||||
const daily = data.daily
|
|
||||||
const currentUnits = data.current_units || {}
|
|
||||||
|
|
||||||
const tempC = current.temperature_2m || 0
|
|
||||||
const tempF = SettingsData.useFahrenheit ? tempC : (tempC * 9/5 + 32)
|
|
||||||
const feelsLikeC = current.apparent_temperature || tempC
|
|
||||||
const feelsLikeF = SettingsData.useFahrenheit ? feelsLikeC : (feelsLikeC * 9/5 + 32)
|
|
||||||
|
|
||||||
const forecast = []
|
|
||||||
if (daily.time && daily.time.length > 0) {
|
|
||||||
for (let i = 0; i < Math.min(daily.time.length, 7); i++) {
|
|
||||||
const tempMinC = daily.temperature_2m_min?.[i] || 0
|
|
||||||
const tempMaxC = daily.temperature_2m_max?.[i] || 0
|
|
||||||
const tempMinF = SettingsData.useFahrenheit ? tempMinC : (tempMinC * 9/5 + 32)
|
|
||||||
const tempMaxF = SettingsData.useFahrenheit ? tempMaxC : (tempMaxC * 9/5 + 32)
|
|
||||||
|
|
||||||
forecast.push({
|
|
||||||
"day": formatForecastDay(daily.time[i], i),
|
|
||||||
"wCode": daily.weather_code?.[i] || 0,
|
|
||||||
"tempMin": Math.round(tempMinC),
|
|
||||||
"tempMax": Math.round(tempMaxC),
|
|
||||||
"tempMinF": Math.round(tempMinF),
|
|
||||||
"tempMaxF": Math.round(tempMaxF),
|
|
||||||
"precipitationProbability": Math.round(daily.precipitation_probability_max?.[i] || 0),
|
|
||||||
"sunrise": daily.sunrise?.[i] ? formatTime(daily.sunrise[i]) : "",
|
|
||||||
"sunset": daily.sunset?.[i] ? formatTime(daily.sunset[i]) : ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
root.weather = {
|
|
||||||
"available": true,
|
|
||||||
"loading": false,
|
|
||||||
"temp": Math.round(tempC),
|
|
||||||
"tempF": Math.round(tempF),
|
|
||||||
"feelsLike": Math.round(feelsLikeC),
|
|
||||||
"feelsLikeF": Math.round(feelsLikeF),
|
|
||||||
"city": root.location?.city || "Unknown",
|
|
||||||
"country": root.location?.country || "Unknown",
|
|
||||||
"wCode": current.weather_code || 0,
|
|
||||||
"humidity": Math.round(current.relative_humidity_2m || 0),
|
|
||||||
"wind": Math.round(current.wind_speed_10m || 0) + " " + (currentUnits.wind_speed_10m || 'm/s'),
|
|
||||||
"sunrise": formatTime(daily.sunrise?.[0]) || "06:00",
|
|
||||||
"sunset": formatTime(daily.sunset?.[0]) || "18:00",
|
|
||||||
"uv": 0,
|
|
||||||
"pressure": Math.round(current.surface_pressure || 0),
|
|
||||||
"precipitationProbability": Math.round(current.precipitation || 0),
|
|
||||||
"isDay": Boolean(current.is_day),
|
|
||||||
"forecast": forecast
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayTemp = SettingsData.useFahrenheit ? root.weather.tempF : root.weather.temp
|
|
||||||
const unit = SettingsData.useFahrenheit ? "°F" : "°C"
|
|
||||||
|
|
||||||
root.handleWeatherSuccess()
|
|
||||||
} catch (e) {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.handleWeatherFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: updateTimer
|
|
||||||
interval: nextInterval()
|
|
||||||
running: root.refCount > 0 && SettingsData.weatherEnabled
|
|
||||||
repeat: true
|
|
||||||
triggeredOnStart: true
|
|
||||||
onTriggered: {
|
|
||||||
root.fetchWeather()
|
|
||||||
interval = nextInterval()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: retryTimer
|
|
||||||
interval: root.retryDelay
|
|
||||||
running: false
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
root.fetchWeather()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: persistentRetryTimer
|
|
||||||
interval: 60000
|
|
||||||
running: false
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (!root.weather.available) {
|
|
||||||
root.weather.loading = true
|
|
||||||
}
|
|
||||||
root.fetchWeather()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
|
|
||||||
SettingsData.weatherCoordinatesChanged.connect(() => {
|
|
||||||
root.location = null
|
|
||||||
root.weather = {
|
|
||||||
"available": false,
|
|
||||||
"loading": true,
|
|
||||||
"temp": 0,
|
|
||||||
"tempF": 0,
|
|
||||||
"feelsLike": 0,
|
|
||||||
"feelsLikeF": 0,
|
|
||||||
"city": "",
|
|
||||||
"country": "",
|
|
||||||
"wCode": 0,
|
|
||||||
"humidity": 0,
|
|
||||||
"wind": "",
|
|
||||||
"sunrise": "06:00",
|
|
||||||
"sunset": "18:00",
|
|
||||||
"uv": 0,
|
|
||||||
"pressure": 0,
|
|
||||||
"precipitationProbability": 0,
|
|
||||||
"isDay": true,
|
|
||||||
"forecast": []
|
|
||||||
}
|
|
||||||
root.lastFetchTime = 0
|
|
||||||
root.forceRefresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
SettingsData.weatherLocationChanged.connect(() => {
|
|
||||||
root.location = null
|
|
||||||
root.lastFetchTime = 0
|
|
||||||
root.forceRefresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
SettingsData.useAutoLocationChanged.connect(() => {
|
|
||||||
root.location = null
|
|
||||||
root.weather = {
|
|
||||||
"available": false,
|
|
||||||
"loading": true,
|
|
||||||
"temp": 0,
|
|
||||||
"tempF": 0,
|
|
||||||
"feelsLike": 0,
|
|
||||||
"feelsLikeF": 0,
|
|
||||||
"city": "",
|
|
||||||
"country": "",
|
|
||||||
"wCode": 0,
|
|
||||||
"humidity": 0,
|
|
||||||
"wind": "",
|
|
||||||
"sunrise": "06:00",
|
|
||||||
"sunset": "18:00",
|
|
||||||
"uv": 0,
|
|
||||||
"pressure": 0,
|
|
||||||
"precipitationProbability": 0,
|
|
||||||
"isDay": true,
|
|
||||||
"forecast": []
|
|
||||||
}
|
|
||||||
root.lastFetchTime = 0
|
|
||||||
root.forceRefresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
SettingsData.useFahrenheitChanged.connect(() => {
|
|
||||||
root.lastFetchTime = 0
|
|
||||||
root.forceRefresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
SettingsData.weatherEnabledChanged.connect(() => {
|
|
||||||
if (SettingsData.weatherEnabled && root.refCount > 0 && !root.weather.available) {
|
|
||||||
root.forceRefresh()
|
|
||||||
} else if (!SettingsData.weatherEnabled) {
|
|
||||||
updateTimer.stop()
|
|
||||||
retryTimer.stop()
|
|
||||||
persistentRetryTimer.stop()
|
|
||||||
if (weatherFetcher.running) {
|
|
||||||
weatherFetcher.running = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,56 +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
|
|
||||||
|
|
||||||
signal rotationCompleted()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: rotationTimer
|
|
||||||
interval: 16
|
|
||||||
repeat: false
|
|
||||||
onTriggered: icon.rotationCompleted()
|
|
||||||
}
|
|
||||||
|
|
||||||
onRotationChanged: {
|
|
||||||
rotationTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property alias content: contentLoader.sourceComponent
|
|
||||||
property alias contentLoader: contentLoader
|
|
||||||
property var modelData
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property int autoHideInterval: 2000
|
|
||||||
property bool enableMouseInteraction: false
|
|
||||||
property real osdWidth: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
property real osdHeight: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
property int animationDuration: Theme.mediumDuration
|
|
||||||
property var animationEasing: Theme.emphasizedEasing
|
|
||||||
|
|
||||||
signal osdShown
|
|
||||||
signal osdHidden
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
hideTimer.restart()
|
|
||||||
osdShown()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
shouldBeVisible = false
|
|
||||||
closeTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetHideTimer() {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
hideTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHoverState() {
|
|
||||||
let isHovered = (enableMouseInteraction && mouseArea.containsMouse) || osdContainer.childHovered
|
|
||||||
if (enableMouseInteraction) {
|
|
||||||
if (isHovered) {
|
|
||||||
hideTimer.stop()
|
|
||||||
} else if (shouldBeVisible) {
|
|
||||||
hideTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setChildHovered(hovered) {
|
|
||||||
osdContainer.childHovered = hovered
|
|
||||||
updateHoverState()
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: modelData
|
|
||||||
visible: false
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: hideTimer
|
|
||||||
|
|
||||||
interval: autoHideInterval
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (!enableMouseInteraction || !mouseArea.containsMouse) {
|
|
||||||
hide()
|
|
||||||
} else {
|
|
||||||
hideTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
interval: animationDuration + 50
|
|
||||||
onTriggered: {
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
visible = false
|
|
||||||
osdHidden()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: osdContainer
|
|
||||||
|
|
||||||
property bool childHovered: false
|
|
||||||
|
|
||||||
width: osdWidth
|
|
||||||
height: osdHeight
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: Theme.spacingM
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
opacity: shouldBeVisible ? 1 : 0
|
|
||||||
scale: shouldBeVisible ? 1 : 0.9
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: enableMouseInteraction
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
z: -1
|
|
||||||
onContainsMouseChanged: updateHoverState()
|
|
||||||
}
|
|
||||||
|
|
||||||
onChildHoveredChanged: updateHoverState()
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.visible
|
|
||||||
asynchronous: false
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 4
|
|
||||||
shadowBlur: 0.8
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: animationDuration
|
|
||||||
easing.type: animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: animationDuration
|
|
||||||
easing.type: animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mask: Region {
|
|
||||||
item: osdContainer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "quickshell:popout"
|
|
||||||
|
|
||||||
property alias content: contentLoader.sourceComponent
|
|
||||||
property alias contentLoader: contentLoader
|
|
||||||
property real popupWidth: 400
|
|
||||||
property real popupHeight: 300
|
|
||||||
property real triggerX: 0
|
|
||||||
property real triggerY: 0
|
|
||||||
property real triggerWidth: 40
|
|
||||||
property string triggerSection: ""
|
|
||||||
property string positioning: "center"
|
|
||||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
|
||||||
property real animationScaleCollapsed: 0.96
|
|
||||||
property real animationOffset: Theme.spacingL
|
|
||||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
|
||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property int keyboardFocusMode: WlrKeyboardFocus.OnDemand
|
|
||||||
|
|
||||||
signal opened
|
|
||||||
signal popoutClosed
|
|
||||||
signal backgroundClicked
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
opened()
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
shouldBeVisible = false
|
|
||||||
closeTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible)
|
|
||||||
close()
|
|
||||||
else
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
interval: animationDuration + 120
|
|
||||||
onTriggered: {
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
visible = false
|
|
||||||
popoutClosed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: shouldBeVisible ? keyboardFocusMode : WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real screenWidth: root.screen.width
|
|
||||||
readonly property real screenHeight: root.screen.height
|
|
||||||
readonly property real dpr: {
|
|
||||||
if (CompositorService.isNiri && root.screen) {
|
|
||||||
const niriScale = NiriService.displayScales[root.screen.name]
|
|
||||||
if (niriScale !== undefined) return niriScale
|
|
||||||
}
|
|
||||||
if (CompositorService.isHyprland && root.screen) {
|
|
||||||
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === root.screen.name)
|
|
||||||
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
|
|
||||||
}
|
|
||||||
return root.screen?.devicePixelRatio || 1
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
|
||||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
|
||||||
readonly property real alignedX: Theme.snap((() => {
|
|
||||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left) {
|
|
||||||
return triggerY + SettingsData.dankBarBottomGap
|
|
||||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
|
||||||
return screenWidth - triggerY - SettingsData.dankBarBottomGap - popupWidth
|
|
||||||
} else {
|
|
||||||
const centerX = triggerX + (triggerWidth / 2) - (popupWidth / 2)
|
|
||||||
return Math.max(Theme.popupDistance, Math.min(screenWidth - popupWidth - Theme.popupDistance, centerX))
|
|
||||||
}
|
|
||||||
})(), dpr)
|
|
||||||
readonly property real alignedY: Theme.snap((() => {
|
|
||||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
|
||||||
const centerY = triggerX + (triggerWidth / 2) - (popupHeight / 2)
|
|
||||||
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, centerY))
|
|
||||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Bottom) {
|
|
||||||
return Math.max(Theme.popupDistance, screenHeight - triggerY - popupHeight)
|
|
||||||
} else {
|
|
||||||
return Math.min(screenHeight - popupHeight - Theme.popupDistance, triggerY)
|
|
||||||
}
|
|
||||||
})(), dpr)
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: shouldBeVisible
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.x < alignedX || mouse.x > alignedX + alignedWidth ||
|
|
||||||
mouse.y < alignedY || mouse.y > alignedY + alignedHeight) {
|
|
||||||
backgroundClicked()
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
x: alignedX
|
|
||||||
y: alignedY
|
|
||||||
width: alignedWidth
|
|
||||||
height: alignedHeight
|
|
||||||
active: root.visible
|
|
||||||
asynchronous: false
|
|
||||||
transformOrigin: Item.Center
|
|
||||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true"
|
|
||||||
layer.smooth: true
|
|
||||||
opacity: shouldBeVisible ? 1 : 0
|
|
||||||
transform: [scaleTransform, motionTransform]
|
|
||||||
|
|
||||||
Scale {
|
|
||||||
id: scaleTransform
|
|
||||||
|
|
||||||
origin.x: contentLoader.width / 2
|
|
||||||
origin.y: contentLoader.height / 2
|
|
||||||
xScale: root.shouldBeVisible ? 1 : root.animationScaleCollapsed
|
|
||||||
yScale: root.shouldBeVisible ? 1 : root.animationScaleCollapsed
|
|
||||||
|
|
||||||
Behavior on xScale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on yScale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Translate {
|
|
||||||
id: motionTransform
|
|
||||||
|
|
||||||
readonly property bool barTop: SettingsData.dankBarPosition === SettingsData.Position.Top
|
|
||||||
readonly property bool barBottom: SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
|
||||||
readonly property bool barLeft: SettingsData.dankBarPosition === SettingsData.Position.Left
|
|
||||||
readonly property bool barRight: SettingsData.dankBarPosition === SettingsData.Position.Right
|
|
||||||
readonly property real hiddenX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
|
||||||
readonly property real hiddenY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
|
||||||
|
|
||||||
x: Theme.snap(root.shouldBeVisible ? 0 : hiddenX, root.dpr)
|
|
||||||
y: Theme.snap(root.shouldBeVisible ? 0 : hiddenY, root.dpr)
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
x: alignedX
|
|
||||||
y: alignedY
|
|
||||||
width: alignedWidth
|
|
||||||
height: alignedHeight
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
close()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component.onCompleted: forceActiveFocus()
|
|
||||||
onVisibleChanged: if (visible) forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "quickshell:slideout"
|
|
||||||
|
|
||||||
property bool isVisible: false
|
|
||||||
property var targetScreen: null
|
|
||||||
property var modelData: null
|
|
||||||
property real slideoutWidth: 480
|
|
||||||
property bool expandable: false
|
|
||||||
property bool expandedWidth: false
|
|
||||||
property real expandedWidthValue: 960
|
|
||||||
property Component content: null
|
|
||||||
property string title: ""
|
|
||||||
property alias container: contentContainer
|
|
||||||
property real customTransparency: -1
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
visible = true
|
|
||||||
isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (isVisible) {
|
|
||||||
hide()
|
|
||||||
} else {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: isVisible
|
|
||||||
screen: modelData
|
|
||||||
|
|
||||||
anchors.top: true
|
|
||||||
anchors.bottom: true
|
|
||||||
anchors.right: true
|
|
||||||
|
|
||||||
implicitWidth: expandable ? expandedWidthValue : slideoutWidth
|
|
||||||
implicitHeight: modelData ? modelData.height : 800
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
|
||||||
WlrLayershell.exclusiveZone: 0
|
|
||||||
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
id: contentRect
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.right: parent.right
|
|
||||||
width: expandable && expandedWidth ? expandedWidthValue : slideoutWidth
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b,
|
|
||||||
customTransparency >= 0 ? customTransparency : SettingsData.popupTransparency)
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
visible: isVisible || slideAnimation.running
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
id: slideTransform
|
|
||||||
x: isVisible ? 0 : contentRect.width
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
id: slideAnimation
|
|
||||||
duration: 450
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
|
|
||||||
onRunningChanged: {
|
|
||||||
if (!running && !isVisible) {
|
|
||||||
root.visible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 250
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: headerColumn
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: root.title !== ""
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - buttonRow.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.title
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: buttonRow
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: expandButton
|
|
||||||
iconName: root.expandedWidth ? "unfold_less" : "unfold_more"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
visible: root.expandable
|
|
||||||
onClicked: root.expandedWidth = !root.expandedWidth
|
|
||||||
|
|
||||||
transform: Rotation {
|
|
||||||
angle: 90
|
|
||||||
origin.x: expandButton.width / 2
|
|
||||||
origin.y: expandButton.height / 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: closeButton
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: contentContainer
|
|
||||||
anchors.top: root.title !== "" ? headerColumn.bottom : parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.topMargin: root.title !== "" ? 0 : Theme.spacingL
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
anchors.bottomMargin: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
sourceComponent: root.content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
property string colorOverride: ""
|
|
||||||
property real brightnessOverride: 0.5
|
|
||||||
property real contrastOverride: 1
|
|
||||||
|
|
||||||
readonly property bool hasColorOverride: colorOverride !== ""
|
|
||||||
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
layer.enabled: hasColorOverride
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $LOGO"], (output, exitCode) => {
|
|
||||||
if (exitCode !== 0) return
|
|
||||||
const logo = output.trim()
|
|
||||||
if (logo === "cachyos") {
|
|
||||||
source = "file:///usr/share/icons/cachyos.svg"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
source = Quickshell.iconPath(logo, true)
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
colorization: 1
|
|
||||||
colorizationColor: colorOverride
|
|
||||||
brightness: brightnessOverride
|
|
||||||
contrast: contrastOverride
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user