mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 23:42:51 -05:00
Compare commits
1573 Commits
v0.0.6
...
f236706d6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f236706d6a | ||
|
|
b097700591 | ||
|
|
50b112c9d6 | ||
|
|
c2f478b088 | ||
|
|
dccbb137d7 | ||
|
|
90f9940dbd | ||
|
|
f3f7cc9077 | ||
|
|
c331e2f39e | ||
|
|
1c7ebc4323 | ||
|
|
5f5427266f | ||
|
|
33e655becd | ||
|
|
0ea0602aec | ||
|
|
46effd2ca4 | ||
|
|
de055e8260 | ||
|
|
c3077304af | ||
|
|
e15135911f | ||
|
|
d430cae944 | ||
|
|
f92dc6f71b | ||
|
|
a679be68b1 | ||
|
|
c5c5ce8409 | ||
|
|
e7cb0d397e | ||
|
|
b84308cb49 | ||
|
|
0df47d2ce3 | ||
|
|
e24b548b54 | ||
|
|
75af444cee | ||
|
|
02dd19962f | ||
|
|
f552b8ef7b | ||
|
|
9162e31489 | ||
|
|
01b28e3ee8 | ||
|
|
f5aa855125 | ||
|
|
db3610fcdb | ||
|
|
2e3f330058 | ||
|
|
1617a7f2c1 | ||
|
|
69a5566bf9 | ||
|
|
30e5d8b855 | ||
|
|
67ff7726e0 | ||
|
|
f96a2e2325 | ||
|
|
344c4f9385 | ||
|
|
89aa146845 | ||
|
|
468e569bc7 | ||
|
|
139c99001a | ||
|
|
bd99be15c2 | ||
|
|
1d91d8fd94 | ||
|
|
f425f86101 | ||
|
|
83a6b7567f | ||
|
|
9184c70883 | ||
|
|
f5ca4ccce5 | ||
|
|
50f174be92 | ||
|
|
e5d11ce535 | ||
|
|
94851a51aa | ||
|
|
cfc07f4411 | ||
|
|
c6e9abda9f | ||
|
|
25951ddc55 | ||
|
|
bcd9ece077 | ||
|
|
68adbc38ba | ||
|
|
79a4d06cc0 | ||
|
|
18bf3b7548 | ||
|
|
4e66d3532e | ||
|
|
1b6d567451 | ||
|
|
7959a79575 | ||
|
|
abf3249b67 | ||
|
|
35e0dc84e8 | ||
|
|
17639e8729 | ||
|
|
cbd1fd908c | ||
|
|
b2cf20f3d8 | ||
|
|
915f1a5036 | ||
|
|
a55ec6416c | ||
|
|
b1834b1958 | ||
|
|
1beeb9fb55 | ||
|
|
18d86354ec | ||
|
|
6297b0679c | ||
|
|
d62ef635a7 | ||
|
|
c53836040f | ||
|
|
0b638bf85f | ||
|
|
7f6a71b964 | ||
|
|
1b4363a54a | ||
|
|
16d168c970 | ||
|
|
4606d7960e | ||
|
|
4eee126d26 | ||
|
|
dde426658f | ||
|
|
f6874fbcad | ||
|
|
621d4e4d92 | ||
|
|
76062231fd | ||
|
|
261f55fea5 | ||
|
|
202cf4bcc9 | ||
|
|
b7572f727f | ||
|
|
50ab346d58 | ||
|
|
b11b375848 | ||
|
|
e6c3ae9397 | ||
|
|
df663aceb9 | ||
|
|
db7e597f67 | ||
|
|
1d3fe81ff7 | ||
|
|
9c887fbe63 | ||
|
|
4723bffcd2 | ||
|
|
9643de3ca0 | ||
|
|
3bf3a54916 | ||
|
|
bcffc8856a | ||
|
|
6b8c35c27b | ||
|
|
dd409b4d1c | ||
|
|
94a1aebe2b | ||
|
|
d3030c3ec6 | ||
|
|
0221021078 | ||
|
|
966021bfd4 | ||
|
|
f06e6e85d5 | ||
|
|
28ad641070 | ||
|
|
384c775f1a | ||
|
|
ce40c691e9 | ||
|
|
5b0c38b0ed | ||
|
|
734456785f | ||
|
|
4f24312432 | ||
|
|
d79b1ff3b4 | ||
|
|
bbe1c1f1e0 | ||
|
|
1978e67401 | ||
|
|
e129e4a2d0 | ||
|
|
f7f1bbbdd2 | ||
|
|
de8f2e6a68 | ||
|
|
85704e3947 | ||
|
|
4d661ff41d | ||
|
|
d7b39634e6 | ||
|
|
039c98b9e3 | ||
|
|
172c4bf0a9 | ||
|
|
1f2a1c5dec | ||
|
|
e5a6a00282 | ||
|
|
d8153f7611 | ||
|
|
8b6ae3f39b | ||
|
|
24537781b7 | ||
|
|
d2a29506aa | ||
|
|
adf51d5264 | ||
|
|
0864179085 | ||
|
|
8de77f283d | ||
|
|
004a014000 | ||
|
|
80f6eb94aa | ||
|
|
4035c9cc5f | ||
|
|
3a365f6807 | ||
|
|
9920a0a59f | ||
|
|
c17bb9e171 | ||
|
|
03073f6875 | ||
|
|
609caf6e5f | ||
|
|
411141ff88 | ||
|
|
3e472e18bd | ||
|
|
e5b6fbd12a | ||
|
|
c2787f1282 | ||
|
|
df940124b1 | ||
|
|
5288d042ca | ||
|
|
fa98a27c90 | ||
|
|
d341a5a60b | ||
|
|
7f15227de1 | ||
|
|
bb45240665 | ||
|
|
29f84aeab5 | ||
|
|
5a52edcad8 | ||
|
|
b078e23aa1 | ||
|
|
7fa87125b5 | ||
|
|
f618df46d8 | ||
|
|
ee03853901 | ||
|
|
6c4a9bcfb8 | ||
|
|
1bec20ecef | ||
|
|
08c9bf570d | ||
|
|
5e77a10a81 | ||
|
|
3bc6461e2a | ||
|
|
d3194e15e2 | ||
|
|
2db79ef202 | ||
|
|
b3c07edef6 | ||
|
|
b773fdca34 | ||
|
|
2e9f9f7b7e | ||
|
|
30cbfe729d | ||
|
|
b036da2446 | ||
|
|
c8a9fb1674 | ||
|
|
43bea80cad | ||
|
|
23538c0323 | ||
|
|
2ae911230d | ||
|
|
5ce1cb87ea | ||
|
|
2a37028b6a | ||
|
|
8130feb2a0 | ||
|
|
c49a875ec2 | ||
|
|
2a002304b9 | ||
|
|
d9522818ae | ||
|
|
800588e121 | ||
|
|
991c31ebdb | ||
|
|
48f77e1691 | ||
|
|
42de6fd074 | ||
|
|
62845b470c | ||
|
|
fd20986cf8 | ||
|
|
61369cde9e | ||
|
|
644384ce8b | ||
|
|
97c11a2482 | ||
|
|
1e7e1c2d78 | ||
|
|
1c7201fb04 | ||
|
|
61ec0c697a | ||
|
|
4b5fce1bfc | ||
|
|
6cc6e7c8e9 | ||
|
|
89298fce30 | ||
|
|
a3a27e07fa | ||
|
|
4f32376f22 | ||
|
|
58bf189941 | ||
|
|
bcfa508da5 | ||
|
|
c0ae3ef58b | ||
|
|
1e70d7b4c3 | ||
|
|
f8dc6ad2bc | ||
|
|
e22482988f | ||
|
|
4eb896629d | ||
|
|
b310e66275 | ||
|
|
b39da1bea7 | ||
|
|
fa575d0574 | ||
|
|
dfe2f3771b | ||
|
|
46caeb0445 | ||
|
|
59cc9c7006 | ||
|
|
12e91534eb | ||
|
|
d9da88ceb5 | ||
|
|
2dbfec0307 | ||
|
|
09cf8c9641 | ||
|
|
f1bed4d6a3 | ||
|
|
2ed6c33c83 | ||
|
|
7ad532ed17 | ||
|
|
92fe8c5b14 | ||
|
|
8e95572589 | ||
|
|
62da862a66 | ||
|
|
993e34f548 | ||
|
|
e39465aece | ||
|
|
8fd616b680 | ||
|
|
cc054b27de | ||
|
|
dfdaa82245 | ||
|
|
99a307e0ad | ||
|
|
5ddea836a1 | ||
|
|
208d92aa06 | ||
|
|
6ef9ddd4f3 | ||
|
|
1c92d39185 | ||
|
|
c0f072217c | ||
|
|
542562f988 | ||
|
|
4e6f0d5e87 | ||
|
|
10639a5ead | ||
|
|
06d668e710 | ||
|
|
d1472dfcba | ||
|
|
ccb4da3cd8 | ||
|
|
46e96b49f0 | ||
|
|
984cfe7f98 | ||
|
|
d769300137 | ||
|
|
d175d66828 | ||
|
|
c1a314332e | ||
|
|
046ac59d21 | ||
|
|
00c06f07d0 | ||
|
|
3e2ab40c6a | ||
|
|
350ffd0052 | ||
|
|
ecd1a622d2 | ||
|
|
f13968aa61 | ||
|
|
4d1ffde54c | ||
|
|
d69017a706 | ||
|
|
f2deaeccdb | ||
|
|
ea9b0d2a79 | ||
|
|
2e6dbedb8b | ||
|
|
6f359df8f9 | ||
|
|
f6db20cd06 | ||
|
|
6287fae065 | ||
|
|
e441607ce3 | ||
|
|
b5379a95fa | ||
|
|
64ec5be919 | ||
|
|
3916512d66 | ||
|
|
e2f426a1bd | ||
|
|
aa1df8dfcf | ||
|
|
67557555f2 | ||
|
|
4cb652abd9 | ||
|
|
d11868b99f | ||
|
|
1798417e6a | ||
|
|
43dc3e5bb1 | ||
|
|
91891a14ed | ||
|
|
20f7d60147 | ||
|
|
7e17e7d37a | ||
|
|
cbb244f785 | ||
|
|
1c264d858b | ||
|
|
217037c2ae | ||
|
|
b4dbd0b69c | ||
|
|
89a2b5c00b | ||
|
|
929b6dae1a | ||
|
|
52fe493da9 | ||
|
|
3e6be3e762 | ||
|
|
7a8cc449b9 | ||
|
|
8f5a9d6e9f | ||
|
|
1c5e31fea9 | ||
|
|
fd08ae18ab | ||
|
|
a7eb3de06e | ||
|
|
8902dd7c44 | ||
|
|
6387d8400c | ||
|
|
597cacb9cc | ||
|
|
3e285ad9ff | ||
|
|
cc1fa89790 | ||
|
|
b0ed007751 | ||
|
|
e1e2650d2b | ||
|
|
b23f17b633 | ||
|
|
818e40b2df | ||
|
|
5685e39631 | ||
|
|
72534b7674 | ||
|
|
328490d23d | ||
|
|
97a0696930 | ||
|
|
cb4e0660e0 | ||
|
|
67c642de4c | ||
|
|
0d7c2e1024 | ||
|
|
16a779a41b | ||
|
|
c4ca3c8644 | ||
|
|
aabcbe34f3 | ||
|
|
f06626e441 | ||
|
|
c4e1a71776 | ||
|
|
77e6c16bd2 | ||
|
|
9d1fac3570 | ||
|
|
b7aeaa7fc5 | ||
|
|
f6d8c9ff61 | ||
|
|
0490794d6c | ||
|
|
335c83dd3c | ||
|
|
91da720c26 | ||
|
|
b6ac744a68 | ||
|
|
526c4092fd | ||
|
|
ed06dda384 | ||
|
|
6465b11e9b | ||
|
|
b2879878a1 | ||
|
|
3e17b086fb | ||
|
|
0545e6bcda | ||
|
|
27a907433f | ||
|
|
69616800e3 | ||
|
|
abf1f53432 | ||
|
|
881c5f75cb | ||
|
|
4e45796ade | ||
|
|
1ce4ea5230 | ||
|
|
f2a2437baa | ||
|
|
508dc9db1e | ||
|
|
a914e3557f | ||
|
|
f489dc062f | ||
|
|
a7e09f4850 | ||
|
|
8ea97530d4 | ||
|
|
13ab54e83a | ||
|
|
4bc40325cb | ||
|
|
58d9355ea3 | ||
|
|
d46b7528e7 | ||
|
|
1858597fc9 | ||
|
|
83cce5afe4 | ||
|
|
201bd8dc1f | ||
|
|
b62ba69060 | ||
|
|
5d2f5557e5 | ||
|
|
cf75c1aad0 | ||
|
|
76a60df88b | ||
|
|
9322c79b4e | ||
|
|
12365edcf0 | ||
|
|
5efc1f9dad | ||
|
|
ab976cbb24 | ||
|
|
db584b7897 | ||
|
|
0fdc0748cf | ||
|
|
2e79c21dc2 | ||
|
|
5490a230bd | ||
|
|
a6b059b30d | ||
|
|
712e6011aa | ||
|
|
68f6f87410 | ||
|
|
50cdd68b7b | ||
|
|
e8510b925e | ||
|
|
24e800501a | ||
|
|
6013c994a6 | ||
|
|
46c90628b9 | ||
|
|
d2d2dac5d1 | ||
|
|
fd3e7470f4 | ||
|
|
b79e9f72ce | ||
|
|
77eb5dd3bf | ||
|
|
b17c14a07b | ||
|
|
494d90be22 | ||
|
|
da7e599e65 | ||
|
|
e3b7360f39 | ||
|
|
367130882d | ||
|
|
d8563ba79d | ||
|
|
e527453964 | ||
|
|
88fe3c5fbd | ||
|
|
748faf92c1 | ||
|
|
0126aded78 | ||
|
|
695a75ea09 | ||
|
|
80e690f9fc | ||
|
|
e8770b90ef | ||
|
|
eec9da42bf | ||
|
|
1c8f0d6292 | ||
|
|
b753c8840b | ||
|
|
95589982a5 | ||
|
|
37a10bd453 | ||
|
|
7abc76e92c | ||
|
|
7aa4467bda | ||
|
|
471938adb6 | ||
|
|
201a7e3b34 | ||
|
|
11ec3723c3 | ||
|
|
75eb736856 | ||
|
|
8fea126c20 | ||
|
|
cc02d09c4d | ||
|
|
af95631a1d | ||
|
|
7b3d2ab85a | ||
|
|
c52df96af9 | ||
|
|
dee5fa60af | ||
|
|
5e99fdd9c9 | ||
|
|
eb01fe757b | ||
|
|
c52483da2c | ||
|
|
2714c0f4ad | ||
|
|
bba21408ea | ||
|
|
47c5320d67 | ||
|
|
b5c49573e5 | ||
|
|
0197961175 | ||
|
|
f08b98dcba | ||
|
|
3878998080 | ||
|
|
5fa117db4c | ||
|
|
caa085a646 | ||
|
|
392a1c03c5 | ||
|
|
1524d27f4c | ||
|
|
d309957927 | ||
|
|
e0f2c03b91 | ||
|
|
1e5848e0d5 | ||
|
|
18bb7dc47b | ||
|
|
0ea7de12a5 | ||
|
|
c8e382e2dd | ||
|
|
84e19f8565 | ||
|
|
f597ea9948 | ||
|
|
d43e1a7cbe | ||
|
|
8131e713cf | ||
|
|
fefa2bd839 | ||
|
|
cc0984db14 | ||
|
|
f87609417b | ||
|
|
8d57b55f94 | ||
|
|
55776fd7cb | ||
|
|
3963c98689 | ||
|
|
02c59636fc | ||
|
|
989f196894 | ||
|
|
9314de4772 | ||
|
|
44a6cd88cd | ||
|
|
d8774c4787 | ||
|
|
a56066bac1 | ||
|
|
8a96f71d10 | ||
|
|
20a684e8f5 | ||
|
|
c8fcf50095 | ||
|
|
58b637bcca | ||
|
|
86caf92c90 | ||
|
|
35ead280d5 | ||
|
|
5d6c3e364d | ||
|
|
f006175829 | ||
|
|
3e0f325734 | ||
|
|
f8d383cff0 | ||
|
|
f2ec3ae755 | ||
|
|
f95e4e016b | ||
|
|
898e9e67d0 | ||
|
|
15983921b0 | ||
|
|
65c2077e30 | ||
|
|
946a28d3be | ||
|
|
69accb5319 | ||
|
|
4ddcf4391a | ||
|
|
a0d886009a | ||
|
|
91c37aaa96 | ||
|
|
7602247558 | ||
|
|
d5a4035bef | ||
|
|
d9652c7334 | ||
|
|
9b4fd7449b | ||
|
|
bc6b568f7e | ||
|
|
c9ee856f91 | ||
|
|
2355c898f8 | ||
|
|
87d0aed4ad | ||
|
|
3cbc547e0f | ||
|
|
e883ebe307 | ||
|
|
20d797320a | ||
|
|
bc3e043192 | ||
|
|
c0555aa608 | ||
|
|
e75b47c21a | ||
|
|
3702f493f6 | ||
|
|
80d88d4d8f | ||
|
|
95c711ce7e | ||
|
|
0ee89920fd | ||
|
|
fd49a171c0 | ||
|
|
16ad5221eb | ||
|
|
8167c432b8 | ||
|
|
ae5d6c1ba4 | ||
|
|
1e6c80bd03 | ||
|
|
d48cd1acdd | ||
|
|
a5f92165fb | ||
|
|
d834124a71 | ||
|
|
ce6f3afb39 | ||
|
|
f5462fa1bf | ||
|
|
f75e23158a | ||
|
|
253ff71a0a | ||
|
|
8d7db49cb0 | ||
|
|
315509f7a4 | ||
|
|
a7bd8b810b | ||
|
|
a7c8ba332b | ||
|
|
40cadb6a00 | ||
|
|
cbf409dffc | ||
|
|
60a791442e | ||
|
|
528e8bf92e | ||
|
|
8a4243e7f8 | ||
|
|
1ac95f0d14 | ||
|
|
cd51eb25ce | ||
|
|
4e64a2b2b2 | ||
|
|
672c660c41 | ||
|
|
2a56e57490 | ||
|
|
f6efd2363a | ||
|
|
fa08b39bb0 | ||
|
|
81c3110d0d | ||
|
|
c01e636421 | ||
|
|
fd8d2961bf | ||
|
|
9e4b53e20b | ||
|
|
20116b3933 | ||
|
|
bca5ee0c0d | ||
|
|
331bd69021 | ||
|
|
57b11b7699 | ||
|
|
3e9b11c281 | ||
|
|
bbfd618626 | ||
|
|
00abb839f9 | ||
|
|
1d639d5f5a | ||
|
|
c565fc08c3 | ||
|
|
026c71f9fc | ||
|
|
1eed499151 | ||
|
|
21f2aabd58 | ||
|
|
e1f06b7139 | ||
|
|
c4be74bce5 | ||
|
|
7c9e9e1cd9 | ||
|
|
797aabc637 | ||
|
|
630a3d4845 | ||
|
|
3b0bb4ea74 | ||
|
|
e36347a4c3 | ||
|
|
4f59dfc49c | ||
|
|
fc0082a470 | ||
|
|
712449674f | ||
|
|
a64b4527f2 | ||
|
|
71a7ebbfe2 | ||
|
|
d6d701c722 | ||
|
|
43fbbc07f5 | ||
|
|
8504144c32 | ||
|
|
1d3e59b5dd | ||
|
|
3f70ca3506 | ||
|
|
3640d8bd24 | ||
|
|
706a99817f | ||
|
|
4645b2dcab | ||
|
|
13f1673371 | ||
|
|
7d374c4c2a | ||
|
|
5ed449773c | ||
|
|
aef9c2269a | ||
|
|
daa5a3e821 | ||
|
|
5cd1167b28 | ||
|
|
21e7ae3dfd | ||
|
|
5d40138585 | ||
|
|
893fd820a3 | ||
|
|
2d536d99e5 | ||
|
|
a0ee4792b9 | ||
|
|
a8f6880840 | ||
|
|
51296d1d44 | ||
|
|
0f29149014 | ||
|
|
b9f0c277ec | ||
|
|
69964c9704 | ||
|
|
ff1d38e34f | ||
|
|
3abee7f2f5 | ||
|
|
ed0b80008f | ||
|
|
976ff108b3 | ||
|
|
66e3cc77c5 | ||
|
|
229abba1e4 | ||
|
|
8dacaf84cc | ||
|
|
102b185572 | ||
|
|
52ac474f7d | ||
|
|
c0064cfcfa | ||
|
|
414ce5610d | ||
|
|
113ac42814 | ||
|
|
a2f2eef326 | ||
|
|
54a69a6101 | ||
|
|
5e2d3c8d7d | ||
|
|
5a9950a7c3 | ||
|
|
2aadbc1a61 | ||
|
|
749414ab65 | ||
|
|
baaebcd413 | ||
|
|
5a8a60b15d | ||
|
|
f0ddb8db49 | ||
|
|
baa12c0161 | ||
|
|
ca226e98c2 | ||
|
|
453079ef1f | ||
|
|
074aea2c35 | ||
|
|
9cf5f0b9b3 | ||
|
|
89e12eea29 | ||
|
|
03d4caff8f | ||
|
|
89d54dedb7 | ||
|
|
9a9e62ccd3 | ||
|
|
eca38ae920 | ||
|
|
84e89599bf | ||
|
|
e1cdf4ed50 | ||
|
|
e4371ea4fc | ||
|
|
9c45d13cbf | ||
|
|
5f22347d7a | ||
|
|
ca786a3567 | ||
|
|
60ce662d49 | ||
|
|
4f9b0d8925 | ||
|
|
9c2fc570e6 | ||
|
|
0ba982b271 | ||
|
|
ff3123e387 | ||
|
|
1548286083 | ||
|
|
c018d953b8 | ||
|
|
cf66d28774 | ||
|
|
9cec6fd212 | ||
|
|
92926331b5 | ||
|
|
f9932ea222 | ||
|
|
a65d6b7630 | ||
|
|
7252d1e4d7 | ||
|
|
3b5a951431 | ||
|
|
0b1c331705 | ||
|
|
3c354b71f5 | ||
|
|
1eb5f381ae | ||
|
|
c5efd28781 | ||
|
|
53ae8ac917 | ||
|
|
505b6368e6 | ||
|
|
3c20e9e203 | ||
|
|
43427461f5 | ||
|
|
d7740ff6d2 | ||
|
|
1fb4eb33aa | ||
|
|
b27f362b44 | ||
|
|
325e3bc19b | ||
|
|
9215985335 | ||
|
|
293179daa6 | ||
|
|
4fe79dbe85 | ||
|
|
55d738e917 | ||
|
|
986b07f4a9 | ||
|
|
450c2e91ed | ||
|
|
4d06333624 | ||
|
|
fbe4122404 | ||
|
|
baf9b5e6f3 | ||
|
|
c88fc20701 | ||
|
|
b1078d6c73 | ||
|
|
5033d10246 | ||
|
|
986993a890 | ||
|
|
19b13a1e81 | ||
|
|
76637fab33 | ||
|
|
0a79d9a187 | ||
|
|
36b3b3c7ae | ||
|
|
8caeca0c08 | ||
|
|
1c323f54ee | ||
|
|
7ed0b752a8 | ||
|
|
0569906f7c | ||
|
|
2a7cf187ad | ||
|
|
cc5b98a5d2 | ||
|
|
1478c92f49 | ||
|
|
e1785a1738 | ||
|
|
44ebd2918c | ||
|
|
c87fa0de5e | ||
|
|
7b26692c8e | ||
|
|
b294e391e7 | ||
|
|
85f8e362e6 | ||
|
|
d68a6a1056 | ||
|
|
3dae9c0639 | ||
|
|
aede6b064a | ||
|
|
76b168020c | ||
|
|
5e36b1454a | ||
|
|
bd35fbac4d | ||
|
|
e081ec19cc | ||
|
|
d870d8bad6 | ||
|
|
20fd13c836 | ||
|
|
59f98b151d | ||
|
|
4ac1990c12 | ||
|
|
0a5105cc62 | ||
|
|
a9f8b835ee | ||
|
|
0109bd5bda | ||
|
|
01dad64c6d | ||
|
|
ee38f57f6d | ||
|
|
6b163dcb5f | ||
|
|
baadbbc65a | ||
|
|
13a2813db9 | ||
|
|
cfa7d12dd3 | ||
|
|
8bf23d820f | ||
|
|
3c7e903ace | ||
|
|
ee0e3aece9 | ||
|
|
d7efd1b285 | ||
|
|
34f7a7ab18 | ||
|
|
695eb0a401 | ||
|
|
0d44b95a40 | ||
|
|
116c421492 | ||
|
|
53507ef56b | ||
|
|
3c049e031f | ||
|
|
b6688adb35 | ||
|
|
b46fe28c05 | ||
|
|
e7debdcf46 | ||
|
|
2c2930e876 | ||
|
|
ca294fc049 | ||
|
|
86d1a40299 | ||
|
|
7a3884a633 | ||
|
|
7e5c6581c9 | ||
|
|
f17bbbd689 | ||
|
|
24b046e9d7 | ||
|
|
48a7d24c11 | ||
|
|
033f96a4b0 | ||
|
|
f0a1cb6525 | ||
|
|
db5782783b | ||
|
|
29022e260d | ||
|
|
1e1f58d3ed | ||
|
|
12389e2856 | ||
|
|
cde7427449 | ||
|
|
42e7cb7b5f | ||
|
|
d7992bc1f7 | ||
|
|
61c8549401 | ||
|
|
a284dcf61d | ||
|
|
2e462b0899 | ||
|
|
b79c66d59a | ||
|
|
2f2020e7e2 | ||
|
|
b7e99c0d2b | ||
|
|
2648848898 | ||
|
|
79b23ca829 | ||
|
|
0ac5b7bc87 | ||
|
|
1d211e8474 | ||
|
|
1981a83e82 | ||
|
|
cac071e7af | ||
|
|
c6efccd61c | ||
|
|
a90b00e5fe | ||
|
|
7863d03282 | ||
|
|
968606d781 | ||
|
|
f7e8de2556 | ||
|
|
17a8edc1ae | ||
|
|
30dc63c801 | ||
|
|
8db7b8419a | ||
|
|
8c626b20e1 | ||
|
|
a8929c8046 | ||
|
|
f8e4b5e958 | ||
|
|
58cae24157 | ||
|
|
bb4f5f37cc | ||
|
|
237333941a | ||
|
|
e1406875aa | ||
|
|
6ab96898a3 | ||
|
|
7973b2f3da | ||
|
|
90284625af | ||
|
|
6b3442512a | ||
|
|
f9208136af | ||
|
|
9895fbde0d | ||
|
|
30370e4985 | ||
|
|
7a45f370b5 | ||
|
|
38068eeaac | ||
|
|
62df30ed6c | ||
|
|
7e75c9e510 | ||
|
|
42f9edf566 | ||
|
|
c72b6144a5 | ||
|
|
032777e32e | ||
|
|
607b5320fd | ||
|
|
959766b265 | ||
|
|
9774991b56 | ||
|
|
e1587995d0 | ||
|
|
08e6e22046 | ||
|
|
454d8bdc88 | ||
|
|
d0ae7431eb | ||
|
|
d88dc17b21 | ||
|
|
705f569571 | ||
|
|
abc7badfa9 | ||
|
|
e8c2469227 | ||
|
|
1e5e8cd246 | ||
|
|
adc81cfb95 | ||
|
|
e175fa64cb | ||
|
|
f9994d0e42 | ||
|
|
3e5d1c514a | ||
|
|
6310394034 | ||
|
|
f32596053b | ||
|
|
cf4a6969d3 | ||
|
|
0918412916 | ||
|
|
41b1718587 | ||
|
|
ca2acbc704 | ||
|
|
1abd3ef8b1 | ||
|
|
cedba3770c | ||
|
|
f733be1fd1 | ||
|
|
01e02232d7 | ||
|
|
771920b38b | ||
|
|
0f55bbc148 | ||
|
|
ab4e9646ad | ||
|
|
884b73599a | ||
|
|
492c0e7ef7 | ||
|
|
0865ae000b | ||
|
|
049c9b44e4 | ||
|
|
199edd3771 | ||
|
|
8806217d25 | ||
|
|
cc1588debd | ||
|
|
d2ba4b32fe | ||
|
|
b3d5054966 | ||
|
|
57a921425c | ||
|
|
061aaeb933 | ||
|
|
0c7af9c740 | ||
|
|
d5c4b990dc | ||
|
|
a650a79dfc | ||
|
|
7ac6e94348 | ||
|
|
b4abdf3d51 | ||
|
|
b59b87d84e | ||
|
|
799ae1a20e | ||
|
|
1e58e69c59 | ||
|
|
c667bab5ca | ||
|
|
2a744fb174 | ||
|
|
a9744a0cad | ||
|
|
0aab22f242 | ||
|
|
b0f65225a9 | ||
|
|
1311da7258 | ||
|
|
61d68b1f76 | ||
|
|
5dd1769536 | ||
|
|
a45a9bda9e | ||
|
|
4e43c797e2 | ||
|
|
beab1a7b01 | ||
|
|
85c00a9c4e | ||
|
|
b2e5565110 | ||
|
|
95785afec9 | ||
|
|
d9d16eccfe | ||
|
|
26900c9b62 | ||
|
|
8113ddc809 | ||
|
|
3cd6a1a558 | ||
|
|
9128141be0 | ||
|
|
0b0af20a84 | ||
|
|
225144cb46 | ||
|
|
bbe802037e | ||
|
|
1db4e92779 | ||
|
|
072883dcd4 | ||
|
|
a25e929200 | ||
|
|
6c4d27be8a | ||
|
|
8825382502 | ||
|
|
9ce3c5bd73 | ||
|
|
771346c8fa | ||
|
|
a56b2d6a9f | ||
|
|
342f980bad | ||
|
|
dbc1bdeb3b | ||
|
|
1cb10e879e | ||
|
|
d90dd5288b | ||
|
|
e55a517dae | ||
|
|
378def1fa3 | ||
|
|
ea7676c98e | ||
|
|
b9b15568b4 | ||
|
|
701d8cbd8a | ||
|
|
bd525763de | ||
|
|
479868718e | ||
|
|
951136bc4c | ||
|
|
8ab25ef8e4 | ||
|
|
a11cd9b0df | ||
|
|
1e72733e81 | ||
|
|
967b7d05de | ||
|
|
90bc890190 | ||
|
|
647c358b72 | ||
|
|
2a89885437 | ||
|
|
47cc43185d | ||
|
|
aa6e09ed3e | ||
|
|
c7bc3d6f3b | ||
|
|
0b68bf7c07 | ||
|
|
3274ef5e3e | ||
|
|
f93a00c8d4 | ||
|
|
92bcb83b16 | ||
|
|
4e0c813db7 | ||
|
|
d4509c80b7 | ||
|
|
c9313df3a4 | ||
|
|
9b6fb29d46 | ||
|
|
50ce5cf257 | ||
|
|
b19e5b3b40 | ||
|
|
c07ba3f737 | ||
|
|
eff5f60264 | ||
|
|
355b2e16b4 | ||
|
|
c389101a10 | ||
|
|
4aa0b3d0fc | ||
|
|
322f1415f6 | ||
|
|
0d57691e38 | ||
|
|
507b516f89 | ||
|
|
7bf73ab14d | ||
|
|
9a305355c2 | ||
|
|
6ac382a25f | ||
|
|
e02b255442 | ||
|
|
d978740d66 | ||
|
|
62b7492e9f | ||
|
|
c2f42f3f69 | ||
|
|
2c4a40e778 | ||
|
|
635fcad416 | ||
|
|
2c92f830d1 | ||
|
|
4924f3e55a | ||
|
|
53306165e1 | ||
|
|
1ebcdaaf62 | ||
|
|
078ef203b6 | ||
|
|
59669d8b7f | ||
|
|
d38b98459a | ||
|
|
f54e53b8a0 | ||
|
|
851d47213c | ||
|
|
ad778b5d81 | ||
|
|
0cb081a6d0 | ||
|
|
daf3525e80 | ||
|
|
b35198c710 | ||
|
|
1feb77aadb | ||
|
|
d6b690ae2f | ||
|
|
b1ae246c86 | ||
|
|
4ceb5f13e5 | ||
|
|
64960e4dcd | ||
|
|
e1c180a13f | ||
|
|
86a0fd409a | ||
|
|
5a32398446 | ||
|
|
bcb22ec265 | ||
|
|
8719dcf98f | ||
|
|
9b4b2f75c1 | ||
|
|
8acde3a347 | ||
|
|
7c1e247ef8 | ||
|
|
f4cd27d316 | ||
|
|
205c43181b | ||
|
|
05a72abf41 | ||
|
|
14262ba510 | ||
|
|
d847b1e09c | ||
|
|
0086e42a86 | ||
|
|
7474d5a7bf | ||
|
|
5696a36115 | ||
|
|
3cdc1a9c81 | ||
|
|
b095fb9005 | ||
|
|
ce6c16214c | ||
|
|
b6f7f2734e | ||
|
|
4db55e4d77 | ||
|
|
b21f6e80b3 | ||
|
|
a804fb849e | ||
|
|
4ca91cd9f7 | ||
|
|
16e1b587b4 | ||
|
|
5e2756d200 | ||
|
|
ce9ab22ae1 | ||
|
|
72ad35e1f9 | ||
|
|
c0d110cde0 | ||
|
|
b9d5deb2ae | ||
|
|
d4b13ef46b | ||
|
|
748d9e342e | ||
|
|
f49312fc0e | ||
|
|
e0d8bbb243 | ||
|
|
153f2a49f8 | ||
|
|
8b272dc2fd | ||
|
|
87a919bbde | ||
|
|
d3017e98c5 | ||
|
|
5758d7274e | ||
|
|
0e215d69cb | ||
|
|
cbaaa32ce8 | ||
|
|
5c81646397 | ||
|
|
30ca1fb14f | ||
|
|
9fab49984a | ||
|
|
696fa6e4f8 | ||
|
|
921393e84e | ||
|
|
13e894e910 | ||
|
|
7c7e8aaef3 | ||
|
|
7ea3bd9df9 | ||
|
|
7bf7d0afae | ||
|
|
0d329baaca | ||
|
|
941a87b59c | ||
|
|
a9e8ac46d8 | ||
|
|
0d3a294118 | ||
|
|
7a27537632 | ||
|
|
287e778ddb | ||
|
|
ce44edb419 | ||
|
|
9dcd8af7a3 | ||
|
|
76dfcd0ccb | ||
|
|
13a188635d | ||
|
|
cd18fd5aed | ||
|
|
b277bd8014 | ||
|
|
daa0d368ab | ||
|
|
2cc7777e16 | ||
|
|
d276e31f7b | ||
|
|
7f35ba7e21 | ||
|
|
edd54dda84 | ||
|
|
a50a97314d | ||
|
|
4bc05e7083 | ||
|
|
09a45b49a6 | ||
|
|
1c0b71436e | ||
|
|
24f5e9a7e6 | ||
|
|
59d123a4a1 | ||
|
|
ed2afa03f9 | ||
|
|
3c531dc2ec | ||
|
|
83564bd03f | ||
|
|
7146d0d92d | ||
|
|
e842d6761a | ||
|
|
17b405e9dc | ||
|
|
f281513a41 | ||
|
|
63b876479f | ||
|
|
38b833c886 | ||
|
|
d75ea18e9f | ||
|
|
f311b20ef7 | ||
|
|
78f7237422 | ||
|
|
726af3393b | ||
|
|
c772331554 | ||
|
|
80d257b94f | ||
|
|
e2db034959 | ||
|
|
c4e88e5c05 | ||
|
|
e47e7667c6 | ||
|
|
8bb2a64663 | ||
|
|
e056e08fc1 | ||
|
|
342cd55bc0 | ||
|
|
23ef19e683 | ||
|
|
437fd29e96 | ||
|
|
aa7a07fd99 | ||
|
|
5217006dec | ||
|
|
ab4f6baae6 | ||
|
|
1976ea4d49 | ||
|
|
697fc4d2b7 | ||
|
|
38c1f7bbcb | ||
|
|
8cbfaab807 | ||
|
|
f4a4151632 | ||
|
|
5f810fe741 | ||
|
|
adaa0caab8 | ||
|
|
54ef14e765 | ||
|
|
d1383b5d1b | ||
|
|
caa703af99 | ||
|
|
90aab9f4db | ||
|
|
3439030145 | ||
|
|
058c7408d1 | ||
|
|
a4f7fd58f6 | ||
|
|
6f3024c90d | ||
|
|
5f95fa5e79 | ||
|
|
f9cb0506e9 | ||
|
|
2429401d0e | ||
|
|
9ff0d7405f | ||
|
|
5bb5cd296d | ||
|
|
273662e03e | ||
|
|
9c1a89d786 | ||
|
|
524d7ee5c0 | ||
|
|
51d2bc9aae | ||
|
|
0c8a7ff332 | ||
|
|
309b8d9efe | ||
|
|
c2f32b7bdc | ||
|
|
563bc7b359 | ||
|
|
94ca5a5bef | ||
|
|
4464589c0f | ||
|
|
748d4fe2ac | ||
|
|
50b28dc8ca | ||
|
|
118980a9fb | ||
|
|
8aff381676 | ||
|
|
6d0fba1905 | ||
|
|
7e885d3cee | ||
|
|
811daf74ff | ||
|
|
ee755b8bd6 | ||
|
|
1019eb925a | ||
|
|
061bb50b88 | ||
|
|
eb7e665c86 | ||
|
|
80301d1aab | ||
|
|
ea56fb5840 | ||
|
|
692b45c4f0 | ||
|
|
8d53a8826e | ||
|
|
64ea115303 | ||
|
|
b5e29cf50c | ||
|
|
381df1e949 | ||
|
|
13a81eda6f | ||
|
|
a48c39642a | ||
|
|
3be3e622bc | ||
|
|
07fe2ca407 | ||
|
|
9b96dae744 | ||
|
|
0e3d3d1a40 | ||
|
|
cb3274fb0c | ||
|
|
a3ada5b2bb | ||
|
|
3e167a2c52 | ||
|
|
38b3ad2b31 | ||
|
|
56e5cd13b7 | ||
|
|
d63c0fc6f0 | ||
|
|
6814b140fc | ||
|
|
5f7e478118 | ||
|
|
7317024da5 | ||
|
|
9b9fbabc3f | ||
|
|
3c5a23799f | ||
|
|
3bfdc6163c | ||
|
|
cf2f74a38d | ||
|
|
2a7f52c67e | ||
|
|
50fde1e308 | ||
|
|
46fd0ae413 | ||
|
|
65e32dc429 | ||
|
|
413675dfc1 | ||
|
|
a17343f40e | ||
|
|
5d023804c1 | ||
|
|
fa07a846b9 | ||
|
|
5df46b605e | ||
|
|
77cf371a21 | ||
|
|
89802dd040 | ||
|
|
59b95e9dd6 | ||
|
|
b836db5252 | ||
|
|
4dc4b15925 | ||
|
|
5c3062e699 | ||
|
|
3a7777c643 | ||
|
|
71543c35d6 | ||
|
|
f4cf66dc01 | ||
|
|
7870dff0fd | ||
|
|
9fc9c1ed19 | ||
|
|
4d0151350f | ||
|
|
dff10c8d13 | ||
|
|
362bcb9294 | ||
|
|
351b4f8a94 | ||
|
|
90955eb0a1 | ||
|
|
62669747ad | ||
|
|
466d00c666 | ||
|
|
63845ff875 | ||
|
|
d013748a51 | ||
|
|
474af3bc07 | ||
|
|
8e09e155fa | ||
|
|
7f9f4f96b9 | ||
|
|
cd488a8623 | ||
|
|
080b7a28b1 | ||
|
|
6949ed0ebd | ||
|
|
8465fa45bb | ||
|
|
40835ffc89 | ||
|
|
01a42ff330 | ||
|
|
ba49654a64 | ||
|
|
bc6577fe18 | ||
|
|
4ca3f0da67 | ||
|
|
7f2086488b | ||
|
|
3014fd8095 | ||
|
|
27885c8ac3 | ||
|
|
d6be0509ac | ||
|
|
1c85f5e857 | ||
|
|
abe5515aca | ||
|
|
6fba975490 | ||
|
|
2de6798f45 | ||
|
|
04fdfa2a35 | ||
|
|
8f3085290d | ||
|
|
0839fe45f5 | ||
|
|
18f4795fda | ||
|
|
55d9fa622a | ||
|
|
7dc723c764 | ||
|
|
5a63205972 | ||
|
|
a4ceeafb1e | ||
|
|
242e05cc0e | ||
|
|
065dddbe6e | ||
|
|
fa6825252b | ||
|
|
b06e48a444 | ||
|
|
97dbd40f07 | ||
|
|
bc23109f99 | ||
|
|
ecb9675e9c | ||
|
|
e1f9b9e7a4 | ||
|
|
067b485bb3 | ||
|
|
67a4e3074e | ||
|
|
010bc4e8c3 | ||
|
|
9de5e3253e | ||
|
|
e32622ac48 | ||
|
|
5e2371c2cb | ||
|
|
a6ce26ee87 | ||
|
|
2a72c126f1 | ||
|
|
36e1a5d379 | ||
|
|
c12eafa1db | ||
|
|
9e26d8755c | ||
|
|
90bd30e351 | ||
|
|
3fb5d5c4f3 | ||
|
|
9f3685e4d5 | ||
|
|
6565988952 | ||
|
|
10b7a0875b | ||
|
|
bb4b9f1a58 | ||
|
|
981e527560 | ||
|
|
80b2ee719c | ||
|
|
6103d6196f | ||
|
|
ed1a5bfded | ||
|
|
3909ce3350 | ||
|
|
d64cd0b8a4 | ||
|
|
676aa9f93f | ||
|
|
ebd48e2556 | ||
|
|
ad5871aae4 | ||
|
|
e327b1ca5b | ||
|
|
ad43ca11eb | ||
|
|
41ba76e2e2 | ||
|
|
15e773434e | ||
|
|
ed118f4e7a | ||
|
|
3c420f2e30 | ||
|
|
4e271d4f0e | ||
|
|
27f9b3cd0b | ||
|
|
1ed4abd347 | ||
|
|
f71dd1ed54 | ||
|
|
19828d3b06 | ||
|
|
d242e729f0 | ||
|
|
8cd0d5faa5 | ||
|
|
a741d892a9 | ||
|
|
9add3361e0 | ||
|
|
4aac70ab5f | ||
|
|
980aec714a | ||
|
|
af8ee5af0f | ||
|
|
32d9aa0cf2 | ||
|
|
7e49631912 | ||
|
|
43970d34aa | ||
|
|
abb3c40697 | ||
|
|
2757a41102 | ||
|
|
1f8bddaa5e | ||
|
|
4c3b7ca60f | ||
|
|
d9d83e5767 | ||
|
|
b4ebde47be | ||
|
|
ef9f76190d | ||
|
|
71b96efca0 | ||
|
|
7158e09b0e | ||
|
|
8ef125bed2 | ||
|
|
0b11fb2fd5 | ||
|
|
3871f3cf3d | ||
|
|
7c5d1ec0f6 | ||
|
|
b507b08e34 | ||
|
|
1b06090f72 | ||
|
|
5460c20ac3 | ||
|
|
2ccec607a0 | ||
|
|
8a99fcf188 | ||
|
|
1e2489ca76 | ||
|
|
89793d2d62 | ||
|
|
11a1af89f4 | ||
|
|
e24ddb804d | ||
|
|
3524d365be | ||
|
|
2b3b9d037c | ||
|
|
5140cd9d7f | ||
|
|
2df9437b39 | ||
|
|
db440b8a14 | ||
|
|
c3dd70bc99 | ||
|
|
223e783bbc | ||
|
|
ca086dbf16 | ||
|
|
523422cf6c | ||
|
|
2dc310dcbc | ||
|
|
c092cd2921 | ||
|
|
2b14ef76c9 | ||
|
|
fbbf10078f | ||
|
|
2315d423c4 | ||
|
|
9a43465ebf | ||
|
|
fc1444763d | ||
|
|
804bf879ed | ||
|
|
ad44f09421 | ||
|
|
df2469468b | ||
|
|
f8f4fe11eb | ||
|
|
039a370add | ||
|
|
6feebc086d | ||
|
|
bc335c7d72 | ||
|
|
a6dd7254b2 | ||
|
|
7b1026c624 | ||
|
|
4758393cc1 | ||
|
|
52373a3a7d | ||
|
|
c30f9a2841 | ||
|
|
44d6f8f15c | ||
|
|
5ada12f989 | ||
|
|
d213045168 | ||
|
|
d83478239e | ||
|
|
3869955357 | ||
|
|
345d37edf8 | ||
|
|
2788ef28cf | ||
|
|
0d5c1bb3df | ||
|
|
c3d505cdad | ||
|
|
90854e1dd4 | ||
|
|
f96e3b04be | ||
|
|
44449e26a0 | ||
|
|
ddc88fd360 | ||
|
|
fedec450cb | ||
|
|
04ea742830 | ||
|
|
5a5c860cef | ||
|
|
55d06a43f8 | ||
|
|
71eecd6e7b | ||
|
|
6f3019f84b | ||
|
|
e95d3126b2 | ||
|
|
5da265bf0b | ||
|
|
2ce9c43b8c | ||
|
|
740b2f206c | ||
|
|
af622bc7e7 | ||
|
|
7816b50b27 | ||
|
|
4cb7a909f7 | ||
|
|
0fac88e171 | ||
|
|
b4ab9d9650 | ||
|
|
731db13c14 | ||
|
|
414a1ad4d2 | ||
|
|
16055fe96e | ||
|
|
6140c398f0 | ||
|
|
bd02923616 | ||
|
|
6021815fd3 | ||
|
|
8c4aba5479 | ||
|
|
2428b22171 | ||
|
|
a3d30211f6 | ||
|
|
730300d211 | ||
|
|
aaca31276b | ||
|
|
53fb927e36 | ||
|
|
fb5aa0313e | ||
|
|
9b41eecbf1 | ||
|
|
ae461b1caf | ||
|
|
57e36d6710 | ||
|
|
a7c4f09c5b | ||
|
|
554ef16e49 | ||
|
|
082321f860 | ||
|
|
df4f7b8c9e | ||
|
|
3f1742f074 | ||
|
|
4560d5c2d5 | ||
|
|
0ca12d275c | ||
|
|
df9e834309 | ||
|
|
ab1c0bb129 | ||
|
|
5070e4c950 | ||
|
|
c13526ccad | ||
|
|
46e16a6c69 | ||
|
|
53983933dc | ||
|
|
09f3ca39a1 | ||
|
|
42b4c91f35 | ||
|
|
37120776be | ||
|
|
d67bcb66c9 | ||
|
|
acf6e72f35 | ||
|
|
0964b271f5 | ||
|
|
1f7998fc44 | ||
|
|
7ac59f7bd0 | ||
|
|
bf238640af | ||
|
|
6e638cadb7 | ||
|
|
ab0759f441 | ||
|
|
123ec5c78a | ||
|
|
d262c67832 | ||
|
|
fcb9a2838d | ||
|
|
82d1672741 | ||
|
|
657a8b6094 | ||
|
|
165f7e0720 | ||
|
|
91aba84c29 | ||
|
|
02c4ac1bc7 | ||
|
|
c529959027 | ||
|
|
e875d1a5d7 | ||
|
|
d280505b9f | ||
|
|
fdd9d0000b | ||
|
|
74c793eedf | ||
|
|
bd8976c620 | ||
|
|
a3e2563f9b | ||
|
|
a310e3d8fa | ||
|
|
ae9da35af1 | ||
|
|
b45837ff5c | ||
|
|
3dd9ab8a29 | ||
|
|
5ebd2f6b8e | ||
|
|
977043ac92 | ||
|
|
4c6182b79c | ||
|
|
01125e092c | ||
|
|
99ef447a52 | ||
|
|
4722ff9b97 | ||
|
|
23a19df79a | ||
|
|
97b86c6faa | ||
|
|
e6296f20b9 | ||
|
|
0ee9dcc255 | ||
|
|
c7b4e2c49d | ||
|
|
066d1847e4 | ||
|
|
3155bf24c1 | ||
|
|
92bb5b90aa | ||
|
|
a207ed74ff | ||
|
|
9d224113c4 | ||
|
|
934f4b2210 | ||
|
|
a6a41d4de1 | ||
|
|
185ee20f2d | ||
|
|
778b960130 | ||
|
|
184938d10a | ||
|
|
25565af5f9 | ||
|
|
0bd9a4f860 | ||
|
|
fe64dd1dea | ||
|
|
a4a59fd586 | ||
|
|
7ccd2d9418 | ||
|
|
974dd70a06 | ||
|
|
1690e4f63b | ||
|
|
ec75ef468b | ||
|
|
a57c1e6451 | ||
|
|
bdc79a13a9 | ||
|
|
b893694977 | ||
|
|
9be7d44765 | ||
|
|
3f8d8ca379 | ||
|
|
e50c3cceeb | ||
|
|
14e648911d | ||
|
|
3c42a618d4 | ||
|
|
bed2259944 | ||
|
|
7516d44de9 | ||
|
|
e9a61a4f81 | ||
|
|
7a7d7d053a | ||
|
|
2067e44baf | ||
|
|
8e010478c7 | ||
|
|
ec4f0ff2ed | ||
|
|
c04177e45d | ||
|
|
b9b1737639 | ||
|
|
4468e4c6af | ||
|
|
cf5dec733d | ||
|
|
d9b9da4b3d | ||
|
|
d62ef89bc3 | ||
|
|
1b681e68b9 | ||
|
|
d15ee0c29b | ||
|
|
62b7b30754 | ||
|
|
aa52b586d6 | ||
|
|
ca11735c1d | ||
|
|
78683032aa | ||
|
|
bd5064e567 | ||
|
|
3862f0ced5 | ||
|
|
878c1d9385 | ||
|
|
0a5f635a2d | ||
|
|
6a963ed618 | ||
|
|
35f059b1dc | ||
|
|
b7838e497d | ||
|
|
3edfff388c | ||
|
|
0bfaf2a8f8 | ||
|
|
50f6dfa709 | ||
|
|
897f48d151 | ||
|
|
c43f58e1bf | ||
|
|
de532fdeda | ||
|
|
c21896b0df | ||
|
|
1669be4dd9 | ||
|
|
81550229e8 | ||
|
|
f38ffa1de5 | ||
|
|
66b493fb2b | ||
|
|
ceae254160 | ||
|
|
a3b3e49313 | ||
|
|
8fb17f7fd5 | ||
|
|
c464bb3e47 | ||
|
|
bd72cf3adf | ||
|
|
a734d9b497 | ||
|
|
b18814e086 | ||
|
|
74417a18ea | ||
|
|
227081c5c9 | ||
|
|
c3b3edcae8 | ||
|
|
c8f87085a0 | ||
|
|
118375f004 | ||
|
|
babcc66765 | ||
|
|
64c8e79bf2 | ||
|
|
12e8e72bb2 | ||
|
|
72b79c0f51 | ||
|
|
50e440f212 | ||
|
|
7c1c64fefc | ||
|
|
2e4770f4be | ||
|
|
0b2bf0b83d | ||
|
|
1fbb614aa8 | ||
|
|
55a29d8504 | ||
|
|
ffc13f71b7 | ||
|
|
d4816bd174 | ||
|
|
3cac6e5eea | ||
|
|
50bc3db048 | ||
|
|
2b99e61c65 | ||
|
|
306a0c0e2d | ||
|
|
0ba4ee74b5 | ||
|
|
2b5a5115b7 | ||
|
|
8a7e386adb | ||
|
|
6e6412fffc | ||
|
|
bc540056fc | ||
|
|
0055ddbc8d | ||
|
|
3376dc893d | ||
|
|
4a673fe1b5 | ||
|
|
6639f6ead0 | ||
|
|
2ce32e8bb5 | ||
|
|
4d13a3d909 | ||
|
|
691b6da7a7 | ||
|
|
e6265c2f71 | ||
|
|
de9bb43c26 | ||
|
|
1ae9802866 | ||
|
|
0e887408c5 | ||
|
|
88b7657e34 | ||
|
|
5fcffba73e | ||
|
|
542536455b | ||
|
|
97f0acadb6 | ||
|
|
32f3e579e7 | ||
|
|
bc446dabaf | ||
|
|
d8e2a73e0b | ||
|
|
ae6fd42163 | ||
|
|
cc7363eee1 | ||
|
|
38c4b33d15 | ||
|
|
6c81aa0908 | ||
|
|
102cc3c0b6 | ||
|
|
02300024cf | ||
|
|
344761df00 | ||
|
|
c0edaea716 | ||
|
|
75e660f78e | ||
|
|
be4d9cadba | ||
|
|
fdaac6ac8b | ||
|
|
5c540f826e | ||
|
|
55b3bffccd | ||
|
|
0cb2a96b3e | ||
|
|
9f13ac6963 | ||
|
|
19e148f65c | ||
|
|
7d9334916c | ||
|
|
50dedb55ac | ||
|
|
38f61ceae2 | ||
|
|
fff4b8c817 | ||
|
|
0fe76b7575 | ||
|
|
3c4719d430 | ||
|
|
c04e2ae0ff | ||
|
|
fec921df10 | ||
|
|
5186afce06 | ||
|
|
77ea6a9372 | ||
|
|
afb0435b5c | ||
|
|
903ef0cc72 | ||
|
|
e4f86abda9 | ||
|
|
8ee43de145 | ||
|
|
83b29c5ed9 | ||
|
|
cdf4ca3b3b | ||
|
|
a0192e709e | ||
|
|
59c7246989 | ||
|
|
76ee483b27 | ||
|
|
ba6c7ae28c | ||
|
|
07c1677145 | ||
|
|
3db1a2ccf2 | ||
|
|
6d84553da9 | ||
|
|
77bc129dfc | ||
|
|
2c451b9779 | ||
|
|
3c6bb58088 | ||
|
|
0372c44d98 | ||
|
|
8fc40ab12d | ||
|
|
9e1272029e | ||
|
|
27344a47e2 | ||
|
|
c42681b9b9 | ||
|
|
939f47286e | ||
|
|
c8cb5dc146 | ||
|
|
c3759dc542 | ||
|
|
17b49ad1f9 | ||
|
|
63c54b5611 | ||
|
|
988d5c2fc3 | ||
|
|
7a671b586c | ||
|
|
b36da8eba2 | ||
|
|
5b49e36da4 | ||
|
|
2395274714 | ||
|
|
d0cbe689f8 | ||
|
|
4ae157af35 | ||
|
|
fb94dd0c4a | ||
|
|
fb01d1af4b | ||
|
|
9438868706 | ||
|
|
a77f6eb385 | ||
|
|
44ebd6a2ee | ||
|
|
e5083fb3a4 | ||
|
|
5a9c80540e | ||
|
|
5bb489f3c4 | ||
|
|
35ea621a17 | ||
|
|
a4d1b6ad76 | ||
|
|
a67a6c7c1c | ||
|
|
1c7d8a55f3 | ||
|
|
c9d38ec6f3 | ||
|
|
d961c7569b | ||
|
|
70a971ade2 | ||
|
|
5d4ba8b38a | ||
|
|
537b4b3604 | ||
|
|
7105b09dc0 | ||
|
|
e0118987ad | ||
|
|
fca3c1ef96 | ||
|
|
c867090f12 | ||
|
|
ee06338640 | ||
|
|
751aee40a5 | ||
|
|
252cc8f42a | ||
|
|
dc28015313 | ||
|
|
47b6b365a1 | ||
|
|
e2945a6a2a | ||
|
|
3aa8ea55e7 | ||
|
|
2179468caf | ||
|
|
597f748e56 | ||
|
|
ac1d01a190 | ||
|
|
fd623035c7 | ||
|
|
cbcb9a7f89 | ||
|
|
9c2e5561d7 | ||
|
|
9292e15419 | ||
|
|
c78cb3c767 | ||
|
|
3a1a553a7d | ||
|
|
cbf3d0f330 | ||
|
|
9d416ddbd6 | ||
|
|
079faa0d40 | ||
|
|
4c18e6b01d | ||
|
|
fcd818b098 | ||
|
|
cd1c992abd | ||
|
|
cebcb4d5d9 | ||
|
|
68243107ca | ||
|
|
af952f6397 | ||
|
|
7d88f37f90 | ||
|
|
8abc94fd07 | ||
|
|
c23088c98a | ||
|
|
072cbf284e | ||
|
|
f7dfd31256 | ||
|
|
2ef41555a9 | ||
|
|
c9d7641d86 | ||
|
|
5f97fd4f7c | ||
|
|
9ba3bfb4db | ||
|
|
353ee355a3 | ||
|
|
79c21e67dc | ||
|
|
4963658bc2 | ||
|
|
613faea714 | ||
|
|
2d764d8cac | ||
|
|
bd29abc7e3 | ||
|
|
3d3b2726c9 | ||
|
|
0b76171151 | ||
|
|
8d674a4fdc | ||
|
|
68157ca636 | ||
|
|
18484bb9cc | ||
|
|
e02b2580c9 | ||
|
|
0d6dbf5f99 | ||
|
|
ba1125bc00 | ||
|
|
af5094b479 | ||
|
|
4742636eb3 | ||
|
|
c708f57e8f | ||
|
|
54b63a1f5f | ||
|
|
3c3cffd7b6 | ||
|
|
e64124cce3 | ||
|
|
eecbd8c733 | ||
|
|
9e0693ca8c | ||
|
|
998d390161 | ||
|
|
67a16da8c7 | ||
|
|
21867c842f | ||
|
|
f7c45acc26 | ||
|
|
178ccd3634 | ||
|
|
b4e607e2b4 | ||
|
|
3856ce14cd | ||
|
|
d4db8a01fe | ||
|
|
886c6877d5 | ||
|
|
5146dcb3f7 | ||
|
|
73b832eddb | ||
|
|
f5871ab27e | ||
|
|
ae6a1b77c2 | ||
|
|
93da303967 | ||
|
|
2a8087cd27 | ||
|
|
531d6334fb | ||
|
|
21089aa66e | ||
|
|
be42280aca | ||
|
|
123058f770 | ||
|
|
0872499f36 | ||
|
|
cfbeb6062b | ||
|
|
213543acb3 | ||
|
|
5bffb1ba10 | ||
|
|
96db0581d3 | ||
|
|
c4438b3339 | ||
|
|
154b90103e | ||
|
|
809274a294 |
66
.githooks/pre-commit
Executable file
66
.githooks/pre-commit
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env 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']
|
||||||
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -9,23 +9,25 @@ assignees: ""
|
|||||||
<!-- If your issue is related to ICONS
|
<!-- If your issue is related to ICONS
|
||||||
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
||||||
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
||||||
- Follow the [THEMING](https://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
|
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
|
||||||
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
||||||
|
|
||||||
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
|
|
||||||
Quickshell does not ever update its DesktopEntires.
|
|
||||||
There is an open PR for it, that has been stuck unmerged over there to fix it.
|
|
||||||
We unfortunately are at the mercy of quickshell to merge it.
|
|
||||||
Until then, newly installed and removed apps will not react until the
|
|
||||||
shell is restarted.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Compositor
|
## Compositor
|
||||||
|
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
- [ ] Other (specify)
|
- [ ] Other (specify)
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
<!-- Arch, Fedora, Debian, etc. -->
|
||||||
|
|
||||||
|
## dms version
|
||||||
|
|
||||||
|
<!-- Output of dms version command -->
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- Brief description of the issue -->
|
<!-- Brief description of the issue -->
|
||||||
@@ -45,6 +47,14 @@ assignees: ""
|
|||||||
## Error Messages/Logs
|
## Error Messages/Logs
|
||||||
|
|
||||||
<!-- Please include any error messages, stack traces, or relevant logs -->
|
<!-- Please include any error messages, stack traces, or relevant logs -->
|
||||||
|
<!-- you can get a log file with the following steps:
|
||||||
|
dms kill
|
||||||
|
mkdir ~/dms_logs
|
||||||
|
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
|
||||||
|
|
||||||
|
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
```
|
```
|
||||||
Paste error messages or logs here
|
Paste error messages or logs here
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -21,6 +21,8 @@ Is this feature specific to one compositor?
|
|||||||
- [ ] All compositors
|
- [ ] All compositors
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
|
|
||||||
## Proposed Solution
|
## Proposed Solution
|
||||||
|
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/support_request.md
vendored
10
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -10,8 +10,18 @@ assignees: ""
|
|||||||
|
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
- [ ] other
|
- [ ] other
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
<!-- Arch, Fedora, Debian, etc. -->
|
||||||
|
|
||||||
|
## dms version
|
||||||
|
|
||||||
|
<!-- Output of dms version command -->
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- Brief description of the support needed -->
|
<!-- Brief description of the support needed -->
|
||||||
|
|||||||
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
|
||||||
698
.github/workflows/release.yml
vendored
698
.github/workflows/release.yml
vendored
@@ -1,59 +1,701 @@
|
|||||||
name: Create Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref_name }}
|
group: release-${{ github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_release:
|
build-core:
|
||||||
name: 📦 Create GitHub Release
|
runs-on: ubuntu-latest
|
||||||
runs-on: ubuntu-24.04
|
strategy:
|
||||||
steps:
|
matrix:
|
||||||
- uses: actions/checkout@v4
|
arch: [amd64, arm64]
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Fetch full history for changelog generation
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- 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 tests
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: Build dankinstall (${{ matrix.arch }})
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
cd cmd/dankinstall
|
||||||
|
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-o ../../dankinstall-${{ matrix.arch }}
|
||||||
|
cd ../..
|
||||||
|
gzip -9 -k dankinstall-${{ matrix.arch }}
|
||||||
|
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
|
|
||||||
|
- name: Build dms (${{ matrix.arch }})
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
cd cmd/dms
|
||||||
|
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-o ../../dms-${{ matrix.arch }}
|
||||||
|
cd ../..
|
||||||
|
gzip -9 -k dms-${{ matrix.arch }}
|
||||||
|
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
||||||
|
|
||||||
|
- name: Generate shell completions
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
chmod +x dms-amd64
|
||||||
|
./dms-amd64 completion bash > completion.bash
|
||||||
|
./dms-amd64 completion fish > completion.fish
|
||||||
|
./dms-amd64 completion zsh > completion.zsh
|
||||||
|
|
||||||
|
- name: Build dms-distropkg (${{ matrix.arch }})
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
cd cmd/dms
|
||||||
|
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-o ../../dms-distropkg-${{ matrix.arch }}
|
||||||
|
cd ../..
|
||||||
|
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
||||||
|
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||||
|
|
||||||
|
- name: Upload artifacts (${{ matrix.arch }})
|
||||||
|
if: matrix.arch == 'arm64'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: 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
|
||||||
|
|
||||||
# Generate changelog
|
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
run: |
|
run: |
|
||||||
# Get the previous tag
|
set -e
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
||||||
|
|
||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
echo "No previous tag found, using all commits"
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" | head -50)
|
|
||||||
else
|
else
|
||||||
echo "Generating changelog from $PREVIOUS_TAG to HEAD"
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD)
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create the changelog with proper formatting
|
cat > RELEASE_BODY.md << 'EOF'
|
||||||
cat > CHANGELOG.md << EOF
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://install.danklinux.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
### Complete Packages
|
||||||
|
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
|
||||||
|
### Individual Components
|
||||||
|
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||||
|
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||||
|
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||||
|
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||||
|
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||||
|
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||||
|
- **`dms-qml.tar.gz`** - QML source code only
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
||||||
|
|
||||||
|
**Installation:** Extract the `dms-full-*.tar.gz` package for your architecture and follow the `INSTALL.md` instructions inside.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >> RELEASE_BODY.md << EOF
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|
||||||
$CHANGELOG
|
$CHANGELOG
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${{ github.ref_name }}
|
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${TAG}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Set output for use in release step
|
|
||||||
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
||||||
cat CHANGELOG.md >> $GITHUB_OUTPUT
|
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Create GitHub Release
|
- name: Prepare release assets
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
mkdir -p _release_assets
|
||||||
|
|
||||||
|
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
|
||||||
|
for file in _core_assets/dms-*.gz*; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
basename=$(basename "$file")
|
||||||
|
if [[ "$basename" == dms-distropkg-* ]]; then
|
||||||
|
cp "$file" "_release_assets/$basename"
|
||||||
|
else
|
||||||
|
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
|
||||||
|
cp "$file" "_release_assets/$newname"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Copy dankinstall binaries
|
||||||
|
cp _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='*.tar.gz' \
|
||||||
|
-czf ../_release_assets/dms-qml.tar.gz .)
|
||||||
|
|
||||||
|
# Generate checksum for QML package
|
||||||
|
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||||
|
|
||||||
|
# Create full packages for each architecture
|
||||||
|
for arch in amd64 arm64; do
|
||||||
|
mkdir -p _temp_full/dms
|
||||||
|
mkdir -p _temp_full/bin
|
||||||
|
mkdir -p _temp_full/completions
|
||||||
|
|
||||||
|
# Extract QML source
|
||||||
|
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
||||||
|
|
||||||
|
# Add CLI binaries
|
||||||
|
if [ -f "_core_assets/dms-${arch}.gz" ]; then
|
||||||
|
gunzip -c "_core_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||||
|
chmod +x _temp_full/bin/dms
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
|
||||||
|
gunzip -c "_core_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||||
|
chmod +x _temp_full/bin/dms-distropkg
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add shell completions
|
||||||
|
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
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Wayland compositor (niri or Hyprland recommended)
|
||||||
|
- Quickshell framework
|
||||||
|
- Qt6
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
1. **Install quickshell assets:**
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/quickshell
|
||||||
|
cp -r dms ~/.config/quickshell/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install the DMS CLI binaries:**
|
||||||
|
```bash
|
||||||
|
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install shell completions (optional):**
|
||||||
|
```bash
|
||||||
|
# Bash
|
||||||
|
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
||||||
|
|
||||||
|
# Fish
|
||||||
|
sudo install -m 644 completions/completion.fish /usr/share/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
|
# Zsh
|
||||||
|
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start the shell:**
|
||||||
|
```bash
|
||||||
|
dms run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Settings are stored in `~/.config/DankMaterialShell/settings.json`
|
||||||
|
- Plugins go in `~/.config/DankMaterialShell/plugins/`
|
||||||
|
- See the documentation in the `dms/` directory for more details
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- Run with verbose output: `DMS_LOG_LEVEL=debug dms run`
|
||||||
|
- Ensure all dependencies are installed
|
||||||
|
EOFINSTALL
|
||||||
|
|
||||||
|
# Create the full package
|
||||||
|
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
||||||
|
|
||||||
|
# Generate checksum
|
||||||
|
(cd _release_assets && sha256sum "dms-full-${arch}.tar.gz" > "dms-full-${arch}.tar.gz.sha256")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf _temp_full
|
||||||
|
done
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: comnoco/create-release-action@v2.0.5
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ env.TAG }}
|
||||||
|
name: Release ${{ env.TAG }}
|
||||||
|
body: ${{ steps.changelog.outputs.changelog }}
|
||||||
|
files: _release_assets/**
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ contains(env.TAG, '-') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
||||||
|
|
||||||
|
%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:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
release_name: Release ${{ github.ref_name }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
body: ${{ steps.changelog.outputs.changelog }}
|
retention-days: 90
|
||||||
draft: false
|
|
||||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
- name: Install Copr CLI
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y python3-pip
|
||||||
|
pip3 install copr-cli
|
||||||
|
|
||||||
|
mkdir -p ~/.config
|
||||||
|
cat > ~/.config/copr << EOF
|
||||||
|
[copr-cli]
|
||||||
|
login = ${{ secrets.COPR_LOGIN }}
|
||||||
|
username = avengemedia
|
||||||
|
token = ${{ secrets.COPR_TOKEN }}
|
||||||
|
copr_url = https://copr.fedorainfracloud.org
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/copr
|
||||||
|
|
||||||
|
- name: Upload to Copr
|
||||||
|
run: |
|
||||||
|
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
echo "Uploading SRPM to avengemedia/dms..."
|
||||||
|
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||||
|
echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
|
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
|
if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
|
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||||
|
fi
|
||||||
|
|||||||
315
.github/workflows/run-copr.yml
vendored
Normal file
315
.github/workflows/run-copr.yml
vendored
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
name: DMS Copr Stable Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
release:
|
||||||
|
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
|
||||||
|
required: false
|
||||||
|
default: '1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-upload:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
# Get version from manual input or latest release
|
||||||
|
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||||
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
|
echo "Using manual version: $VERSION"
|
||||||
|
else
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||||
|
echo "Using latest release version: $VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE="${{ github.event.inputs.release }}"
|
||||||
|
if [ -z "$RELEASE" ]; then
|
||||||
|
RELEASE="1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||||
|
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
|
||||||
|
|
||||||
|
- name: Setup build environment
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y rpm wget curl jq gzip
|
||||||
|
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
echo "✅ RPM build environment ready"
|
||||||
|
|
||||||
|
- name: Download release assets
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
cd ~/rpmbuild/SOURCES
|
||||||
|
|
||||||
|
echo "📦 Downloading DMS QML source for v${VERSION}..."
|
||||||
|
|
||||||
|
# Download DMS QML source
|
||||||
|
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||||
|
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✅ Source downloaded"
|
||||||
|
echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture"
|
||||||
|
ls -lh
|
||||||
|
|
||||||
|
- name: Generate stable spec file
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
RELEASE="${{ steps.version.outputs.release }}"
|
||||||
|
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: RELEASE_PLACEHOLDER%{?dist}
|
||||||
|
Summary: %{pkg_summary}
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
Source0: dms-qml.tar.gz
|
||||||
|
|
||||||
|
BuildRequires: gzip
|
||||||
|
BuildRequires: wget
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
|
Requires: (quickshell or quickshell-git)
|
||||||
|
Requires: accountsservice
|
||||||
|
Requires: dms-cli
|
||||||
|
Requires: dgop
|
||||||
|
|
||||||
|
Recommends: cava
|
||||||
|
Recommends: cliphist
|
||||||
|
Recommends: danksearch
|
||||||
|
Recommends: hyprpicker
|
||||||
|
Recommends: matugen
|
||||||
|
Recommends: wl-clipboard
|
||||||
|
Recommends: NetworkManager
|
||||||
|
Recommends: qt6-qtmultimedia
|
||||||
|
Suggests: qt6ct
|
||||||
|
|
||||||
|
%description
|
||||||
|
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
||||||
|
and optimized for the niri and hyprland compositors. Features notifications,
|
||||||
|
app launcher, wallpaper customization, and fully customizable with plugins.
|
||||||
|
|
||||||
|
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
||||||
|
process monitoring, notification center, clipboard history, dock, control center,
|
||||||
|
lock screen, and comprehensive plugin system.
|
||||||
|
|
||||||
|
%package -n dms-cli
|
||||||
|
Summary: DankMaterialShell CLI tool
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
%description -n dms-cli
|
||||||
|
Command-line interface for DankMaterialShell configuration and management.
|
||||||
|
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||||
|
|
||||||
|
%package -n dgop
|
||||||
|
Summary: Stateless CPU/GPU monitor for DankMaterialShell
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/AvengeMedia/dgop
|
||||||
|
Provides: dgop
|
||||||
|
|
||||||
|
%description -n dgop
|
||||||
|
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
|
||||||
|
network statistics. Designed for integration with DankMaterialShell but can be
|
||||||
|
used standalone. This package always includes the latest stable dgop release.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup -q -c -n dms-qml
|
||||||
|
|
||||||
|
# Download architecture-specific binaries during build
|
||||||
|
# This ensures the correct architecture is used for each build target
|
||||||
|
case "%{_arch}" in
|
||||||
|
x86_64)
|
||||||
|
ARCH_SUFFIX="amd64"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
ARCH_SUFFIX="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: %{_arch}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Download dms-cli for target architecture
|
||||||
|
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||||
|
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
||||||
|
chmod +x %{_builddir}/dms-cli
|
||||||
|
|
||||||
|
# Download dgop for target architecture
|
||||||
|
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
|
||||||
|
echo "Failed to download dgop for architecture %{_arch}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
|
||||||
|
chmod +x %{_builddir}/dgop
|
||||||
|
|
||||||
|
%build
|
||||||
|
|
||||||
|
%install
|
||||||
|
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
|
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
||||||
|
|
||||||
|
# Shell completions
|
||||||
|
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||||
|
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||||
|
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||||
|
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||||
|
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||||
|
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||||
|
|
||||||
|
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
|
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||||
|
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||||
|
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
|
%posttrans
|
||||||
|
# Clean up old installation path from previous versions (only if empty)
|
||||||
|
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
|
# Remove directories only if empty (preserves any user-added files)
|
||||||
|
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
|
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||||
|
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restart DMS for active users after upgrade
|
||||||
|
if [ "$1" -ge 2 ]; then
|
||||||
|
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
%files
|
||||||
|
%license LICENSE
|
||||||
|
%doc README.md CONTRIBUTING.md
|
||||||
|
%{_datadir}/quickshell/dms/
|
||||||
|
%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
%files -n dms-cli
|
||||||
|
%{_bindir}/dms
|
||||||
|
%{_datadir}/bash-completion/completions/dms
|
||||||
|
%{_datadir}/zsh/site-functions/_dms
|
||||||
|
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
|
%files -n dgop
|
||||||
|
%{_bindir}/dgop
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
||||||
|
- 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/RELEASE_PLACEHOLDER/${RELEASE}/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}-${RELEASE}"
|
||||||
|
echo ""
|
||||||
|
echo "=== Spec file preview ==="
|
||||||
|
head -40 ~/rpmbuild/SPECS/dms.spec
|
||||||
|
|
||||||
|
- name: Build SRPM
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
cd ~/rpmbuild/SPECS
|
||||||
|
|
||||||
|
echo "🔨 Building SRPM..."
|
||||||
|
rpmbuild -bs dms.spec
|
||||||
|
|
||||||
|
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
||||||
|
SRPM_NAME=$(basename "$SRPM")
|
||||||
|
|
||||||
|
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||||
|
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "✅ SRPM built: $SRPM_NAME"
|
||||||
|
echo ""
|
||||||
|
echo "=== SRPM Info ==="
|
||||||
|
rpm -qpi "$SRPM"
|
||||||
|
|
||||||
|
- name: Upload SRPM artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
- name: Install Copr CLI
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y python3-pip
|
||||||
|
pip3 install copr-cli
|
||||||
|
|
||||||
|
mkdir -p ~/.config
|
||||||
|
cat > ~/.config/copr << EOF
|
||||||
|
[copr-cli]
|
||||||
|
login = ${{ secrets.COPR_LOGIN }}
|
||||||
|
username = avengemedia
|
||||||
|
token = ${{ secrets.COPR_TOKEN }}
|
||||||
|
copr_url = https://copr.fedorainfracloud.org
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/copr
|
||||||
|
|
||||||
|
echo "✅ Copr CLI configured"
|
||||||
|
|
||||||
|
- name: Upload to Copr
|
||||||
|
run: |
|
||||||
|
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
echo "🚀 Uploading SRPM to avengemedia/dms..."
|
||||||
|
echo " SRPM: $(basename $SRPM)"
|
||||||
|
echo " Version: $VERSION"
|
||||||
|
|
||||||
|
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||||
|
echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
|
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
|
if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
|
echo "✅ Build submitted successfully!"
|
||||||
|
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||||
|
else
|
||||||
|
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
||||||
276
.github/workflows/run-obs.yml
vendored
Normal file
276
.github/workflows/run-obs.yml
vendored
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
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 current commit hash (8 chars to match spec format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Check OBS for last uploaded commit
|
||||||
|
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||||
|
mkdir -p "$OBS_BASE"
|
||||||
|
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||||
|
|
||||||
|
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||||
|
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||||
|
osc up -q 2>/dev/null || true
|
||||||
|
|
||||||
|
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
||||||
|
if [[ -f "dms-git.spec" ]]; then
|
||||||
|
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$OBS_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract OBS commit, proceeding with update"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No spec file in OBS, proceeding with update"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${{ github.workspace }}"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 First upload to OBS, update needed"
|
||||||
|
fi
|
||||||
|
elif [[ -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 Debian dms-git changelog 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")
|
||||||
|
|
||||||
|
# Debian version format: 0.6.2+git2256.9162e314
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
|
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
|
||||||
|
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
# Get current version from changelog
|
||||||
|
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
||||||
|
|
||||||
|
echo "Current Debian version: $CURRENT_VERSION"
|
||||||
|
echo "New version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Only update if version changed
|
||||||
|
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||||
|
# Create new changelog entry at top
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
|
||||||
|
cat > "$TEMP_CHANGELOG" << EOF
|
||||||
|
dms-git ($NEW_VERSION) nightly; urgency=medium
|
||||||
|
|
||||||
|
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
||||||
|
|
||||||
|
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Prepend to existing changelog
|
||||||
|
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
||||||
|
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
||||||
|
else
|
||||||
|
echo "✓ Debian changelog already at version $NEW_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update dms stable version
|
||||||
|
if: steps.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
|
||||||
188
.github/workflows/run-ppa.yml
vendored
Normal file
188
.github/workflows/run-ppa.yml
vendored
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
name: Update PPA Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
|
||||||
|
required: false
|
||||||
|
default: 'dms-git'
|
||||||
|
rebuild_release:
|
||||||
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-updates:
|
||||||
|
name: Check for updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check for updates
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Checking if dms-git source has changed..."
|
||||||
|
|
||||||
|
# Get current commit hash (8 chars to match changelog format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Extract commit hash from changelog
|
||||||
|
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
||||||
|
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
if [[ -f "$CHANGELOG_FILE" ]]; then
|
||||||
|
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No changelog file found, proceeding with upload"
|
||||||
|
fi
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
upload-ppa:
|
||||||
|
name: Upload to PPA
|
||||||
|
needs: check-updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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: 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=${{ needs.check-updates.outputs.packages }}" >> $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=${{ needs.check-updates.outputs.packages }}" >> $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
|
||||||
|
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||||
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
|
||||||
47
.gitignore
vendored
47
.gitignore
vendored
@@ -27,7 +27,6 @@ qrc_*.cpp
|
|||||||
ui_*.h
|
ui_*.h
|
||||||
*.qmlc
|
*.qmlc
|
||||||
*.jsc
|
*.jsc
|
||||||
Makefile*
|
|
||||||
*build-*
|
*build-*
|
||||||
*.qm
|
*.qm
|
||||||
*.prl
|
*.prl
|
||||||
@@ -63,6 +62,11 @@ CLAUDE-temp.md
|
|||||||
niri-colors.generated.kdl
|
niri-colors.generated.kdl
|
||||||
ghostty-colors.generated.conf
|
ghostty-colors.generated.conf
|
||||||
|
|
||||||
|
# Notepad files (should be in ~/.local/state/DankMaterialShell/)
|
||||||
|
untitled-*.txt
|
||||||
|
file:*
|
||||||
|
notepad-files/
|
||||||
|
|
||||||
result
|
result
|
||||||
|
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
@@ -97,3 +101,44 @@ 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-*/
|
||||||
|
|||||||
52
CONTRIBUTING.md
Normal file
52
CONTRIBUTING.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Contributions are welcome and encouraged.
|
||||||
|
|
||||||
|
To contribute fork this repository, make your changes, and open a pull request.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Enable pre-commit hooks to catch CI failures before pushing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath .githooks
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
|
||||||
@@ -1,55 +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 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"
|
|
||||||
return appId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,390 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isLightMode: false
|
|
||||||
property string wallpaperPath: ""
|
|
||||||
property string wallpaperLastPath: ""
|
|
||||||
property string profileLastPath: ""
|
|
||||||
property bool doNotDisturb: false
|
|
||||||
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 string nightModeLocationProvider: ""
|
|
||||||
property var pinnedApps: []
|
|
||||||
property int selectedGpuIndex: 0
|
|
||||||
property bool nvidiaGpuTempEnabled: false
|
|
||||||
property bool nonNvidiaGpuTempEnabled: false
|
|
||||||
property var enabledGpuPciIds: []
|
|
||||||
property bool wallpaperCyclingEnabled: false
|
|
||||||
property string wallpaperCyclingMode: "interval" // "interval" or "time"
|
|
||||||
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
|
|
||||||
property string wallpaperCyclingTime: "06:00" // HH:mm format
|
|
||||||
property string lastBrightnessDevice: ""
|
|
||||||
property string notepadContent: ""
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
loadSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSettings() {
|
|
||||||
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 : ""
|
|
||||||
wallpaperLastPath = settings.wallpaperLastPath
|
|
||||||
!== undefined ? settings.wallpaperLastPath : ""
|
|
||||||
profileLastPath = settings.profileLastPath
|
|
||||||
!== undefined ? settings.profileLastPath : ""
|
|
||||||
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"
|
|
||||||
// Handle legacy time format
|
|
||||||
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
|
|
||||||
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"
|
|
||||||
lastBrightnessDevice = settings.lastBrightnessDevice
|
|
||||||
!== undefined ? settings.lastBrightnessDevice : ""
|
|
||||||
notepadContent = settings.notepadContent !== undefined ? settings.notepadContent : ""
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSettings() {
|
|
||||||
settingsFile.setText(JSON.stringify({
|
|
||||||
"isLightMode": isLightMode,
|
|
||||||
"wallpaperPath": wallpaperPath,
|
|
||||||
"wallpaperLastPath": wallpaperLastPath,
|
|
||||||
"profileLastPath": profileLastPath,
|
|
||||||
"doNotDisturb": doNotDisturb,
|
|
||||||
"nightModeEnabled": nightModeEnabled,
|
|
||||||
"nightModeTemperature": nightModeTemperature,
|
|
||||||
"nightModeAutoEnabled": nightModeAutoEnabled,
|
|
||||||
"nightModeAutoMode": nightModeAutoMode,
|
|
||||||
"nightModeStartHour": nightModeStartHour,
|
|
||||||
"nightModeStartMinute": nightModeStartMinute,
|
|
||||||
"nightModeEndHour": nightModeEndHour,
|
|
||||||
"nightModeEndMinute": nightModeEndMinute,
|
|
||||||
"latitude": latitude,
|
|
||||||
"longitude": longitude,
|
|
||||||
"nightModeLocationProvider": nightModeLocationProvider,
|
|
||||||
"pinnedApps": pinnedApps,
|
|
||||||
"selectedGpuIndex": selectedGpuIndex,
|
|
||||||
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
|
|
||||||
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
|
|
||||||
"enabledGpuPciIds": enabledGpuPciIds,
|
|
||||||
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
|
|
||||||
"wallpaperCyclingMode": wallpaperCyclingMode,
|
|
||||||
"wallpaperCyclingInterval": wallpaperCyclingInterval,
|
|
||||||
"wallpaperCyclingTime": wallpaperCyclingTime,
|
|
||||||
"lastBrightnessDevice": lastBrightnessDevice,
|
|
||||||
"notepadContent": notepadContent
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLightMode(lightMode) {
|
|
||||||
isLightMode = lightMode
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDoNotDisturb(enabled) {
|
|
||||||
doNotDisturb = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeEnabled(enabled) {
|
|
||||||
nightModeEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeTemperature(temperature) {
|
|
||||||
nightModeTemperature = temperature
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeAutoEnabled(enabled) {
|
|
||||||
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
|
|
||||||
nightModeAutoEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeAutoMode(mode) {
|
|
||||||
nightModeAutoMode = mode
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeStartHour(hour) {
|
|
||||||
nightModeStartHour = hour
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeStartMinute(minute) {
|
|
||||||
nightModeStartMinute = minute
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeEndHour(hour) {
|
|
||||||
nightModeEndHour = hour
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeEndMinute(minute) {
|
|
||||||
nightModeEndMinute = minute
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLatitude(lat) {
|
|
||||||
console.log("SessionData: Setting latitude to", lat)
|
|
||||||
latitude = lat
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLongitude(lng) {
|
|
||||||
console.log("SessionData: Setting longitude to", lng)
|
|
||||||
longitude = lng
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNightModeLocationProvider(provider) {
|
|
||||||
nightModeLocationProvider = provider
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperPath(path) {
|
|
||||||
wallpaperPath = path
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaper(imagePath) {
|
|
||||||
console.log("SessionData.setWallpaper called with:", imagePath)
|
|
||||||
wallpaperPath = imagePath
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
console.log("Theme is available, current theme:", Theme.currentTheme)
|
|
||||||
// Always extract colors for shell UI if dynamic theming is enabled
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.wallpaperDynamicTheming) {
|
|
||||||
console.log("Dynamic theming enabled, extracting colors")
|
|
||||||
Theme.extractColors()
|
|
||||||
}
|
|
||||||
// Always generate system themes (matugen templates) when wallpaper changes
|
|
||||||
console.log("Calling generateSystemThemesFromCurrentTheme")
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
} else {
|
|
||||||
console.log("Theme is undefined!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperLastPath(path) {
|
|
||||||
wallpaperLastPath = path
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setProfileLastPath(path) {
|
|
||||||
profileLastPath = path
|
|
||||||
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 setSelectedGpuIndex(index) {
|
|
||||||
selectedGpuIndex = index
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNvidiaGpuTempEnabled(enabled) {
|
|
||||||
nvidiaGpuTempEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNonNvidiaGpuTempEnabled(enabled) {
|
|
||||||
nonNvidiaGpuTempEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setEnabledGpuPciIds(pciIds) {
|
|
||||||
enabledGpuPciIds = pciIds
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperCyclingEnabled(enabled) {
|
|
||||||
wallpaperCyclingEnabled = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperCyclingMode(mode) {
|
|
||||||
wallpaperCyclingMode = mode
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperCyclingInterval(interval) {
|
|
||||||
wallpaperCyclingInterval = interval
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWallpaperCyclingTime(time) {
|
|
||||||
wallpaperCyclingTime = time
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastBrightnessDevice(device) {
|
|
||||||
lastBrightnessDevice = device
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: settingsFile
|
|
||||||
|
|
||||||
path: StandardPaths.writableLocation(
|
|
||||||
StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
|
||||||
blockLoading: true
|
|
||||||
blockWrites: true
|
|
||||||
watchChanges: true
|
|
||||||
onLoaded: {
|
|
||||||
parseSettings(settingsFile.text())
|
|
||||||
}
|
|
||||||
onLoadFailed: error => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "wallpaper"
|
|
||||||
|
|
||||||
function get(): string {
|
|
||||||
return root.wallpaperPath || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function set(path: string): string {
|
|
||||||
if (!path) {
|
|
||||||
return "ERROR: No path provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
var absolutePath = path.startsWith(
|
|
||||||
"/") ? path : StandardPaths.writableLocation(
|
|
||||||
StandardPaths.HomeLocation) + "/" + path
|
|
||||||
|
|
||||||
try {
|
|
||||||
root.setWallpaper(absolutePath)
|
|
||||||
return "SUCCESS: Wallpaper set to " + absolutePath
|
|
||||||
} catch (e) {
|
|
||||||
return "ERROR: Failed to set wallpaper: " + e.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear(): string {
|
|
||||||
root.setWallpaper("")
|
|
||||||
return "SUCCESS: Wallpaper cleared"
|
|
||||||
}
|
|
||||||
|
|
||||||
function next(): string {
|
|
||||||
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.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,380 +0,0 @@
|
|||||||
// Stock theme definitions for DankMaterialShell
|
|
||||||
// Separated from Theme.qml to keep that file clean
|
|
||||||
|
|
||||||
const StockThemes = {
|
|
||||||
DARK: {
|
|
||||||
blue: {
|
|
||||||
name: "Blue",
|
|
||||||
primary: "#42a5f5",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#1976d2",
|
|
||||||
secondary: "#8ab4f8",
|
|
||||||
surface: "#1a1c1e",
|
|
||||||
surfaceText: "#e3e8ef",
|
|
||||||
surfaceVariant: "#44464f",
|
|
||||||
surfaceVariantText: "#c4c7c5",
|
|
||||||
surfaceTint: "#8ab4f8",
|
|
||||||
background: "#1a1c1e",
|
|
||||||
backgroundText: "#e3e8ef",
|
|
||||||
outline: "#8e918f",
|
|
||||||
surfaceContainer: "#1e2023",
|
|
||||||
surfaceContainerHigh: "#292b2f"
|
|
||||||
},
|
|
||||||
deepBlue: {
|
|
||||||
name: "Deep Blue",
|
|
||||||
primary: "#0061a4",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#004881",
|
|
||||||
secondary: "#42a5f5",
|
|
||||||
surface: "#1a1c1e",
|
|
||||||
surfaceText: "#e3e8ef",
|
|
||||||
surfaceVariant: "#44464f",
|
|
||||||
surfaceVariantText: "#c4c7c5",
|
|
||||||
surfaceTint: "#8ab4f8",
|
|
||||||
background: "#1a1c1e",
|
|
||||||
backgroundText: "#e3e8ef",
|
|
||||||
outline: "#8e918f",
|
|
||||||
surfaceContainer: "#1e2023",
|
|
||||||
surfaceContainerHigh: "#292b2f"
|
|
||||||
},
|
|
||||||
purple: {
|
|
||||||
name: "Purple",
|
|
||||||
primary: "#D0BCFF",
|
|
||||||
primaryText: "#381E72",
|
|
||||||
primaryContainer: "#4F378B",
|
|
||||||
secondary: "#CCC2DC",
|
|
||||||
surface: "#10121E",
|
|
||||||
surfaceText: "#E6E0E9",
|
|
||||||
surfaceVariant: "#49454F",
|
|
||||||
surfaceVariantText: "#CAC4D0",
|
|
||||||
surfaceTint: "#D0BCFF",
|
|
||||||
background: "#10121E",
|
|
||||||
backgroundText: "#E6E0E9",
|
|
||||||
outline: "#938F99",
|
|
||||||
surfaceContainer: "#1D1B20",
|
|
||||||
surfaceContainerHigh: "#2B2930"
|
|
||||||
},
|
|
||||||
green: {
|
|
||||||
name: "Green",
|
|
||||||
primary: "#4caf50",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#388e3c",
|
|
||||||
secondary: "#81c995",
|
|
||||||
surface: "#0f1411",
|
|
||||||
surfaceText: "#e1f5e3",
|
|
||||||
surfaceVariant: "#404943",
|
|
||||||
surfaceVariantText: "#c1cbc4",
|
|
||||||
surfaceTint: "#81c995",
|
|
||||||
background: "#0f1411",
|
|
||||||
backgroundText: "#e1f5e3",
|
|
||||||
outline: "#8b938c",
|
|
||||||
surfaceContainer: "#1a1f1b",
|
|
||||||
surfaceContainerHigh: "#252a26"
|
|
||||||
},
|
|
||||||
orange: {
|
|
||||||
name: "Orange",
|
|
||||||
primary: "#ff6d00",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#e65100",
|
|
||||||
secondary: "#ffb74d",
|
|
||||||
surface: "#1c1410",
|
|
||||||
surfaceText: "#f5f1ea",
|
|
||||||
surfaceVariant: "#4a453a",
|
|
||||||
surfaceVariantText: "#cbc5b8",
|
|
||||||
surfaceTint: "#ffb74d",
|
|
||||||
background: "#1c1410",
|
|
||||||
backgroundText: "#f5f1ea",
|
|
||||||
outline: "#958f84",
|
|
||||||
surfaceContainer: "#211e17",
|
|
||||||
surfaceContainerHigh: "#2c291f"
|
|
||||||
},
|
|
||||||
red: {
|
|
||||||
name: "Red",
|
|
||||||
primary: "#f44336",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#d32f2f",
|
|
||||||
secondary: "#f28b82",
|
|
||||||
surface: "#1c1011",
|
|
||||||
surfaceText: "#f5e8ea",
|
|
||||||
surfaceVariant: "#4a3f41",
|
|
||||||
surfaceVariantText: "#cbc2c4",
|
|
||||||
surfaceTint: "#f28b82",
|
|
||||||
background: "#1c1011",
|
|
||||||
backgroundText: "#f5e8ea",
|
|
||||||
outline: "#958b8d",
|
|
||||||
surfaceContainer: "#211b1c",
|
|
||||||
surfaceContainerHigh: "#2c2426"
|
|
||||||
},
|
|
||||||
cyan: {
|
|
||||||
name: "Cyan",
|
|
||||||
primary: "#00bcd4",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#0097a7",
|
|
||||||
secondary: "#4dd0e1",
|
|
||||||
surface: "#0f1617",
|
|
||||||
surfaceText: "#e8f4f5",
|
|
||||||
surfaceVariant: "#3f474a",
|
|
||||||
surfaceVariantText: "#c2c9cb",
|
|
||||||
surfaceTint: "#4dd0e1",
|
|
||||||
background: "#0f1617",
|
|
||||||
backgroundText: "#e8f4f5",
|
|
||||||
outline: "#8c9194",
|
|
||||||
surfaceContainer: "#1a1f20",
|
|
||||||
surfaceContainerHigh: "#252b2c"
|
|
||||||
},
|
|
||||||
pink: {
|
|
||||||
name: "Pink",
|
|
||||||
primary: "#e91e63",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#c2185b",
|
|
||||||
secondary: "#f8bbd9",
|
|
||||||
surface: "#1a1014",
|
|
||||||
surfaceText: "#f3e8ee",
|
|
||||||
surfaceVariant: "#483f45",
|
|
||||||
surfaceVariantText: "#c9c2c7",
|
|
||||||
surfaceTint: "#f8bbd9",
|
|
||||||
background: "#1a1014",
|
|
||||||
backgroundText: "#f3e8ee",
|
|
||||||
outline: "#938a90",
|
|
||||||
surfaceContainer: "#1f1b1e",
|
|
||||||
surfaceContainerHigh: "#2a2428"
|
|
||||||
},
|
|
||||||
amber: {
|
|
||||||
name: "Amber",
|
|
||||||
primary: "#ffc107",
|
|
||||||
primaryText: "#000000",
|
|
||||||
primaryContainer: "#ff8f00",
|
|
||||||
secondary: "#ffd54f",
|
|
||||||
surface: "#1a1710",
|
|
||||||
surfaceText: "#f3f0e8",
|
|
||||||
surfaceVariant: "#49453a",
|
|
||||||
surfaceVariantText: "#cac5b8",
|
|
||||||
surfaceTint: "#ffd54f",
|
|
||||||
background: "#1a1710",
|
|
||||||
backgroundText: "#f3f0e8",
|
|
||||||
outline: "#949084",
|
|
||||||
surfaceContainer: "#1f1e17",
|
|
||||||
surfaceContainerHigh: "#2a281f"
|
|
||||||
},
|
|
||||||
coral: {
|
|
||||||
name: "Coral",
|
|
||||||
primary: "#ffb4ab",
|
|
||||||
primaryText: "#5f1412",
|
|
||||||
primaryContainer: "#8c1d18",
|
|
||||||
secondary: "#f9dedc",
|
|
||||||
surface: "#1a1110",
|
|
||||||
surfaceText: "#f1e8e7",
|
|
||||||
surfaceVariant: "#4a4142",
|
|
||||||
surfaceVariantText: "#cdc2c1",
|
|
||||||
surfaceTint: "#ffb4ab",
|
|
||||||
background: "#1a1110",
|
|
||||||
backgroundText: "#f1e8e7",
|
|
||||||
outline: "#968b8a",
|
|
||||||
surfaceContainer: "#201a19",
|
|
||||||
surfaceContainerHigh: "#2b2221"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LIGHT: {
|
|
||||||
blue: {
|
|
||||||
name: "Blue Light",
|
|
||||||
primary: "#1976d2",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#e3f2fd",
|
|
||||||
secondary: "#42a5f5",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#1976d2",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
|
||||||
deepBlue: {
|
|
||||||
name: "Deep Blue Light",
|
|
||||||
primary: "#0061a4",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#cfe5ff",
|
|
||||||
secondary: "#1976d2",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#0061a4",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
|
||||||
purple: {
|
|
||||||
name: "Purple Light",
|
|
||||||
primary: "#6750A4",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#EADDFF",
|
|
||||||
secondary: "#625B71",
|
|
||||||
surface: "#FFFBFE",
|
|
||||||
surfaceText: "#1C1B1F",
|
|
||||||
surfaceVariant: "#E7E0EC",
|
|
||||||
surfaceVariantText: "#49454F",
|
|
||||||
surfaceTint: "#6750A4",
|
|
||||||
background: "#FFFBFE",
|
|
||||||
backgroundText: "#1C1B1F",
|
|
||||||
outline: "#79747E",
|
|
||||||
surfaceContainer: "#F3EDF7",
|
|
||||||
surfaceContainerHigh: "#ECE6F0"
|
|
||||||
},
|
|
||||||
green: {
|
|
||||||
name: "Green Light",
|
|
||||||
primary: "#2e7d32",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#e8f5e8",
|
|
||||||
secondary: "#4caf50",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#2e7d32",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
|
||||||
orange: {
|
|
||||||
name: "Orange Light",
|
|
||||||
primary: "#e65100",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#ffecb3",
|
|
||||||
secondary: "#ff9800",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#e65100",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
|
||||||
red: {
|
|
||||||
name: "Red Light",
|
|
||||||
primary: "#d32f2f",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#ffebee",
|
|
||||||
secondary: "#f44336",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#d32f2f",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
|
||||||
cyan: {
|
|
||||||
name: "Cyan Light",
|
|
||||||
primary: "#0097a7",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#e0f2f1",
|
|
||||||
secondary: "#00bcd4",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#0097a7",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
|
||||||
pink: {
|
|
||||||
name: "Pink Light",
|
|
||||||
primary: "#c2185b",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#fce4ec",
|
|
||||||
secondary: "#e91e63",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#c2185b",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
|
||||||
amber: {
|
|
||||||
name: "Amber Light",
|
|
||||||
primary: "#ff8f00",
|
|
||||||
primaryText: "#000000",
|
|
||||||
primaryContainer: "#fff8e1",
|
|
||||||
secondary: "#ffc107",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#ff8f00",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
|
||||||
coral: {
|
|
||||||
name: "Coral Light",
|
|
||||||
primary: "#8c1d18",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#ffdad6",
|
|
||||||
secondary: "#ff5449",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#8c1d18",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThemeNames = {
|
|
||||||
BLUE: "blue",
|
|
||||||
DEEP_BLUE: "deepBlue",
|
|
||||||
PURPLE: "purple",
|
|
||||||
GREEN: "green",
|
|
||||||
ORANGE: "orange",
|
|
||||||
RED: "red",
|
|
||||||
CYAN: "cyan",
|
|
||||||
PINK: "pink",
|
|
||||||
AMBER: "amber",
|
|
||||||
CORAL: "coral",
|
|
||||||
DYNAMIC: "dynamic"
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStockTheme(themeName) {
|
|
||||||
return Object.keys(StockThemes.DARK).includes(themeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAvailableThemes(isLight = false) {
|
|
||||||
return isLight ? StockThemes.LIGHT : StockThemes.DARK
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThemeByName(themeName, isLight = false) {
|
|
||||||
const themes = getAvailableThemes(isLight)
|
|
||||||
return themes[themeName] || themes.blue
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllThemeNames() {
|
|
||||||
return Object.keys(StockThemes.DARK)
|
|
||||||
}
|
|
||||||
738
Common/Theme.qml
738
Common/Theme.qml
@@ -1,738 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.UPower
|
|
||||||
import qs.Services
|
|
||||||
import "StockThemes.js" as StockThemes
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string currentTheme: "blue"
|
|
||||||
property bool isLightMode: false
|
|
||||||
|
|
||||||
readonly property string dynamic: "dynamic"
|
|
||||||
|
|
||||||
readonly property string homeDir: {
|
|
||||||
const url = StandardPaths.writableLocation(StandardPaths.HomeLocation).toString()
|
|
||||||
return url.startsWith("file://") ? url.substring(7) : url
|
|
||||||
}
|
|
||||||
readonly property string configDir: {
|
|
||||||
const url = StandardPaths.writableLocation(StandardPaths.ConfigLocation).toString()
|
|
||||||
return url.startsWith("file://") ? url.substring(7) : url
|
|
||||||
}
|
|
||||||
readonly property string shellDir: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Common/", "")
|
|
||||||
readonly property string wallpaperPath: typeof SessionData !== "undefined" ? SessionData.wallpaperPath : ""
|
|
||||||
|
|
||||||
property bool matugenAvailable: false
|
|
||||||
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
|
|
||||||
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
|
|
||||||
property var workerRunning: false
|
|
||||||
property var matugenColors: ({})
|
|
||||||
property bool extractionRequested: false
|
|
||||||
property int colorUpdateTrigger: 0
|
|
||||||
property var customThemeData: null
|
|
||||||
|
|
||||||
readonly property string stateDir: {
|
|
||||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
|
|
||||||
const path = cacheHome.startsWith("file://") ? cacheHome.substring(7) : cacheHome
|
|
||||||
return path + "/dankshell"
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMatugenColor(path, fallback) {
|
|
||||||
colorUpdateTrigger
|
|
||||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
|
||||||
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]
|
|
||||||
for (const part of path.split(".")) {
|
|
||||||
if (!cur || typeof cur !== "object" || !(part in cur))
|
|
||||||
return fallback
|
|
||||||
cur = cur[part]
|
|
||||||
}
|
|
||||||
return cur || fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var currentThemeData: {
|
|
||||||
if (currentTheme === "custom") {
|
|
||||||
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
|
|
||||||
} else if (currentTheme === dynamic) {
|
|
||||||
return {
|
|
||||||
primary: getMatugenColor("primary", "#42a5f5"),
|
|
||||||
primaryText: getMatugenColor("on_primary", "#ffffff"),
|
|
||||||
primaryContainer: getMatugenColor("primary_container", "#1976d2"),
|
|
||||||
secondary: getMatugenColor("secondary", "#8ab4f8"),
|
|
||||||
surface: getMatugenColor("surface", "#1a1c1e"),
|
|
||||||
surfaceText: getMatugenColor("on_background", "#e3e8ef"),
|
|
||||||
surfaceVariant: getMatugenColor("surface_variant", "#44464f"),
|
|
||||||
surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5"),
|
|
||||||
surfaceTint: getMatugenColor("surface_tint", "#8ab4f8"),
|
|
||||||
background: getMatugenColor("background", "#1a1c1e"),
|
|
||||||
backgroundText: getMatugenColor("on_background", "#e3e8ef"),
|
|
||||||
outline: getMatugenColor("outline", "#8e918f"),
|
|
||||||
surfaceContainer: getMatugenColor("surface_container", "#1e2023"),
|
|
||||||
surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f"),
|
|
||||||
error: "#F2B8B5",
|
|
||||||
warning: "#FF9800",
|
|
||||||
info: "#2196F3",
|
|
||||||
success: "#4CAF50"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return StockThemes.getThemeByName(currentTheme, isLightMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property color primary: currentThemeData.primary
|
|
||||||
property color primaryText: currentThemeData.primaryText
|
|
||||||
property color primaryContainer: currentThemeData.primaryContainer
|
|
||||||
property color secondary: currentThemeData.secondary
|
|
||||||
property color surface: currentThemeData.surface
|
|
||||||
property color surfaceText: currentThemeData.surfaceText
|
|
||||||
property color surfaceVariant: currentThemeData.surfaceVariant
|
|
||||||
property color surfaceVariantText: currentThemeData.surfaceVariantText
|
|
||||||
property color surfaceTint: currentThemeData.surfaceTint
|
|
||||||
property color background: currentThemeData.background
|
|
||||||
property color backgroundText: currentThemeData.backgroundText
|
|
||||||
property color outline: currentThemeData.outline
|
|
||||||
property color surfaceContainer: currentThemeData.surfaceContainer
|
|
||||||
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
|
|
||||||
|
|
||||||
property color error: currentThemeData.error || "#F2B8B5"
|
|
||||||
property color warning: currentThemeData.warning || "#FF9800"
|
|
||||||
property color info: currentThemeData.info || "#2196F3"
|
|
||||||
property color tempWarning: "#ff9933"
|
|
||||||
property color tempDanger: "#ff5555"
|
|
||||||
property color success: currentThemeData.success || "#4CAF50"
|
|
||||||
|
|
||||||
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
|
|
||||||
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
|
|
||||||
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
|
|
||||||
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
|
|
||||||
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
|
|
||||||
|
|
||||||
property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08)
|
|
||||||
|
|
||||||
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
|
|
||||||
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
|
|
||||||
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
|
|
||||||
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
|
|
||||||
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
|
|
||||||
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
|
|
||||||
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
|
|
||||||
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
|
|
||||||
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
|
|
||||||
|
|
||||||
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
|
|
||||||
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
|
|
||||||
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
|
|
||||||
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
|
|
||||||
|
|
||||||
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
|
||||||
|
|
||||||
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
|
||||||
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
|
||||||
|
|
||||||
property int shorterDuration: 100
|
|
||||||
property int shortDuration: 150
|
|
||||||
property int mediumDuration: 300
|
|
||||||
property int longDuration: 500
|
|
||||||
property int extraLongDuration: 1000
|
|
||||||
property int standardEasing: Easing.OutCubic
|
|
||||||
property int emphasizedEasing: Easing.OutQuart
|
|
||||||
|
|
||||||
property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
|
|
||||||
property real spacingXS: 4
|
|
||||||
property real spacingS: 8
|
|
||||||
property real spacingM: 12
|
|
||||||
property real spacingL: 16
|
|
||||||
property real spacingXL: 24
|
|
||||||
property real fontSizeSmall: 12
|
|
||||||
property real fontSizeMedium: 14
|
|
||||||
property real fontSizeLarge: 16
|
|
||||||
property real fontSizeXLarge: 20
|
|
||||||
property real barHeight: 48
|
|
||||||
property real iconSize: 24
|
|
||||||
property real iconSizeSmall: 16
|
|
||||||
property real iconSizeLarge: 32
|
|
||||||
|
|
||||||
property real panelTransparency: 0.85
|
|
||||||
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85
|
|
||||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92
|
|
||||||
|
|
||||||
function switchTheme(themeName, savePrefs = true) {
|
|
||||||
if (themeName === dynamic) {
|
|
||||||
currentTheme = dynamic
|
|
||||||
extractColors()
|
|
||||||
} else if (themeName === "custom") {
|
|
||||||
currentTheme = "custom"
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
|
|
||||||
loadCustomThemeFromFile(SettingsData.customThemeFile)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentTheme = themeName
|
|
||||||
}
|
|
||||||
if (savePrefs && typeof SettingsData !== "undefined")
|
|
||||||
SettingsData.setTheme(currentTheme)
|
|
||||||
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLightMode(light, savePrefs = true) {
|
|
||||||
isLightMode = light
|
|
||||||
if (savePrefs && typeof SessionData !== "undefined")
|
|
||||||
SessionData.setLightMode(isLightMode)
|
|
||||||
PortalService.setLightMode(isLightMode)
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleLightMode(savePrefs = true) {
|
|
||||||
setLightMode(!isLightMode, savePrefs)
|
|
||||||
}
|
|
||||||
|
|
||||||
function forceGenerateSystemThemes() {
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showWarning("matugen not available - cannot generate system themes")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAvailableThemes() {
|
|
||||||
return StockThemes.getAllThemeNames()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThemeDisplayName(themeName) {
|
|
||||||
const themeData = StockThemes.getThemeByName(themeName, isLightMode)
|
|
||||||
return themeData.name
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThemeColors(themeName) {
|
|
||||||
if (themeName === "custom" && customThemeData) {
|
|
||||||
return customThemeData
|
|
||||||
}
|
|
||||||
return StockThemes.getThemeByName(themeName, isLightMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCustomTheme(themeData) {
|
|
||||||
if (themeData.dark || themeData.light) {
|
|
||||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
|
||||||
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
|
|
||||||
customThemeData = selectedTheme
|
|
||||||
} else {
|
|
||||||
customThemeData = themeData
|
|
||||||
}
|
|
||||||
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCustomThemeFromFile(filePath) {
|
|
||||||
customThemeFileView.path = filePath
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias availableThemeNames: root._availableThemeNames
|
|
||||||
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
|
|
||||||
property string currentThemeName: currentTheme
|
|
||||||
|
|
||||||
|
|
||||||
function popupBackground() {
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
function contentBackground() {
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
function panelBackground() {
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
function widgetBackground() {
|
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPopupBackgroundAlpha() {
|
|
||||||
return popupTransparency
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContentBackgroundAlpha() {
|
|
||||||
return popupTransparency
|
|
||||||
}
|
|
||||||
|
|
||||||
function isColorDark(c) {
|
|
||||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBatteryIcon(level, isCharging, batteryAvailable) {
|
|
||||||
if (!batteryAvailable)
|
|
||||||
return _getBatteryPowerProfileIcon()
|
|
||||||
|
|
||||||
if (isCharging) {
|
|
||||||
if (level >= 90) return "battery_charging_full"
|
|
||||||
if (level >= 80) return "battery_charging_90"
|
|
||||||
if (level >= 60) return "battery_charging_80"
|
|
||||||
if (level >= 50) return "battery_charging_60"
|
|
||||||
if (level >= 30) return "battery_charging_50"
|
|
||||||
if (level >= 20) return "battery_charging_30"
|
|
||||||
return "battery_charging_20"
|
|
||||||
} else {
|
|
||||||
if (level >= 95) return "battery_full"
|
|
||||||
if (level >= 85) return "battery_6_bar"
|
|
||||||
if (level >= 70) return "battery_5_bar"
|
|
||||||
if (level >= 55) return "battery_4_bar"
|
|
||||||
if (level >= 40) return "battery_3_bar"
|
|
||||||
if (level >= 25) return "battery_2_bar"
|
|
||||||
if (level >= 10) return "battery_1_bar"
|
|
||||||
return "battery_alert"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getBatteryPowerProfileIcon() {
|
|
||||||
if (typeof PowerProfiles === "undefined")
|
|
||||||
return "balance"
|
|
||||||
|
|
||||||
switch (PowerProfiles.profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "energy_savings_leaf"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "rocket_launch"
|
|
||||||
default:
|
|
||||||
return "balance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPowerProfileIcon(profile) {
|
|
||||||
switch (profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "battery_saver"
|
|
||||||
case PowerProfile.Balanced:
|
|
||||||
return "battery_std"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "flash_on"
|
|
||||||
default:
|
|
||||||
return "settings"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPowerProfileLabel(profile) {
|
|
||||||
switch (profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "Power Saver"
|
|
||||||
case PowerProfile.Balanced:
|
|
||||||
return "Balanced"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "Performance"
|
|
||||||
default:
|
|
||||||
return profile.charAt(0).toUpperCase() + profile.slice(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPowerProfileDescription(profile) {
|
|
||||||
switch (profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "Extend battery life"
|
|
||||||
case PowerProfile.Balanced:
|
|
||||||
return "Balance power and performance"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "Prioritize performance"
|
|
||||||
default:
|
|
||||||
return "Custom power profile"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractColors() {
|
|
||||||
extractionRequested = true
|
|
||||||
if (matugenAvailable)
|
|
||||||
fileChecker.running = true
|
|
||||||
else
|
|
||||||
matugenCheck.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLightModeChanged() {
|
|
||||||
if (matugenColors && Object.keys(matugenColors).length > 0) {
|
|
||||||
colorUpdateTrigger++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTheme === "custom" && customThemeFileView.path) {
|
|
||||||
customThemeFileView.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDesiredTheme(kind, value, isLight, iconTheme) {
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
console.warn("matugen not available - cannot set system theme")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const desired = {
|
|
||||||
"kind": kind,
|
|
||||||
"value": value,
|
|
||||||
"mode": isLight ? "light" : "dark",
|
|
||||||
"iconTheme": iconTheme || "System Default"
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = JSON.stringify(desired)
|
|
||||||
const desiredPath = stateDir + "/matugen.desired.json"
|
|
||||||
|
|
||||||
Quickshell.execDetached([
|
|
||||||
"sh", "-c",
|
|
||||||
`mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`
|
|
||||||
])
|
|
||||||
workerRunning = true
|
|
||||||
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"]
|
|
||||||
systemThemeGenerator.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSystemThemesFromCurrentTheme() {
|
|
||||||
if (!matugenAvailable)
|
|
||||||
return
|
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
|
||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
|
||||||
|
|
||||||
if (currentTheme === dynamic) {
|
|
||||||
if (!wallpaperPath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme)
|
|
||||||
} else {
|
|
||||||
let primaryColor
|
|
||||||
if (currentTheme === "custom") {
|
|
||||||
if (!customThemeData || !customThemeData.primary) {
|
|
||||||
console.warn("Custom theme data not available for system theme generation")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
primaryColor = customThemeData.primary
|
|
||||||
} else {
|
|
||||||
primaryColor = currentThemeData.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!primaryColor) {
|
|
||||||
console.warn("No primary color available for theme:", currentTheme)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyGtkColors() {
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("matugen not available - cannot apply GTK colors")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
|
|
||||||
gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir]
|
|
||||||
gtkApplier.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyQtColors() {
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("matugen not available - cannot apply Qt colors")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
qtApplier.command = [shellDir + "/scripts/qt.sh", configDir]
|
|
||||||
qtApplier.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function extractJsonFromText(text) {
|
|
||||||
if (!text) return null
|
|
||||||
|
|
||||||
const start = text.search(/[{\[]/)
|
|
||||||
if (start === -1) return null
|
|
||||||
|
|
||||||
const open = text[start]
|
|
||||||
const pairs = { "{": '}', "[": ']' }
|
|
||||||
const close = pairs[open]
|
|
||||||
if (!close) return null
|
|
||||||
|
|
||||||
let inString = false
|
|
||||||
let escape = false
|
|
||||||
const stack = [open]
|
|
||||||
|
|
||||||
for (var i = start + 1; i < text.length; i++) {
|
|
||||||
const ch = text[i]
|
|
||||||
|
|
||||||
if (inString) {
|
|
||||||
if (escape) {
|
|
||||||
escape = false
|
|
||||||
} else if (ch === '\\') {
|
|
||||||
escape = true
|
|
||||||
} else if (ch === '"') {
|
|
||||||
inString = false
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch === '"') {
|
|
||||||
inString = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch === '{' || ch === '[') {
|
|
||||||
stack.push(ch)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch === '}' || ch === ']') {
|
|
||||||
const last = stack.pop()
|
|
||||||
if (!last || pairs[last] !== ch) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (stack.length === 0) {
|
|
||||||
return text.slice(start, i + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: matugenCheck
|
|
||||||
command: ["which", "matugen"]
|
|
||||||
onExited: code => {
|
|
||||||
matugenAvailable = (code === 0)
|
|
||||||
if (!matugenAvailable) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.wallpaperErrorStatus = "matugen_missing"
|
|
||||||
ToastService.showWarning("matugen not found - dynamic theming disabled")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (extractionRequested) {
|
|
||||||
fileChecker.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
|
||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
|
||||||
|
|
||||||
if (currentTheme === dynamic) {
|
|
||||||
if (wallpaperPath) {
|
|
||||||
// Clear cache on startup to force regeneration
|
|
||||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
|
||||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme)
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let primaryColor
|
|
||||||
if (currentTheme === "custom") {
|
|
||||||
if (customThemeData && customThemeData.primary) {
|
|
||||||
primaryColor = customThemeData.primary
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
primaryColor = currentThemeData.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
if (primaryColor) {
|
|
||||||
// Clear cache on startup to force regeneration
|
|
||||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
|
||||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme)
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: fileChecker
|
|
||||||
command: ["test", "-r", wallpaperPath]
|
|
||||||
onExited: code => {
|
|
||||||
if (code === 0) {
|
|
||||||
matugenProcess.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: matugenProcess
|
|
||||||
command: ["matugen", "image", wallpaperPath, "--json", "hex"]
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
id: matugenCollector
|
|
||||||
onStreamFinished: {
|
|
||||||
if (!matugenCollector.text) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.wallpaperErrorStatus = "error"
|
|
||||||
ToastService.showError("Wallpaper Processing Failed: Empty JSON extracted from matugen output.")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const extractedJson = extractJsonFromText(matugenCollector.text)
|
|
||||||
if (!extractedJson) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.wallpaperErrorStatus = "error"
|
|
||||||
ToastService.showError("Wallpaper Processing Failed: Invalid JSON extracted from matugen output.")
|
|
||||||
}
|
|
||||||
console.log("Raw matugen output:", matugenCollector.text)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
root.matugenColors = JSON.parse(extractedJson)
|
|
||||||
root.colorUpdateTrigger++
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.clearWallpaperError()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.wallpaperErrorStatus = "error"
|
|
||||||
ToastService.showError("Wallpaper processing failed (JSON parse error after extraction)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: code => {
|
|
||||||
if (code !== 0) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.wallpaperErrorStatus = "error"
|
|
||||||
ToastService.showError("Matugen command failed with exit code " + code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ensureStateDir
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: systemThemeGenerator
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
workerRunning = false
|
|
||||||
|
|
||||||
if (exitCode === 2) {
|
|
||||||
// Exit code 2 means wallpaper/color not found - this is expected on first run
|
|
||||||
console.log("Theme worker: wallpaper/color not found, skipping theme generation")
|
|
||||||
} else if (exitCode !== 0) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Theme worker failed (" + exitCode + ")")
|
|
||||||
}
|
|
||||||
console.warn("Theme worker failed with exit code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: gtkApplier
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
id: gtkStdout
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
id: gtkStderr
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showInfo("GTK colors applied successfully")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Failed to apply GTK colors: " + gtkStderr.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: qtApplier
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
id: qtStdout
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
id: qtStderr
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showInfo("Qt colors applied successfully")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Failed to apply Qt colors: " + qtStderr.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
matugenCheck.running = true
|
|
||||||
if (typeof SessionData !== "undefined")
|
|
||||||
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: customThemeFileView
|
|
||||||
watchChanges: currentTheme === "custom"
|
|
||||||
|
|
||||||
function parseAndLoadTheme() {
|
|
||||||
try {
|
|
||||||
var themeData = JSON.parse(customThemeFileView.text())
|
|
||||||
loadCustomTheme(themeData)
|
|
||||||
} catch (e) {
|
|
||||||
ToastService.showError("Invalid JSON format: " + e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
parseAndLoadTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileChanged: {
|
|
||||||
customThemeFileView.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadFailed: function(error) {
|
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showError("Failed to read theme file: " + error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "theme"
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
root.toggleLightMode()
|
|
||||||
return root.isLightMode ? "light" : "dark"
|
|
||||||
}
|
|
||||||
|
|
||||||
function light(): string {
|
|
||||||
root.setLightMode(true)
|
|
||||||
return "light"
|
|
||||||
}
|
|
||||||
|
|
||||||
function dark(): string {
|
|
||||||
root.setLightMode(false)
|
|
||||||
return "dark"
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMode(): string {
|
|
||||||
return root.isLightMode ? "light" : "dark"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,678 +0,0 @@
|
|||||||
.pragma library
|
|
||||||
|
|
||||||
var single = (search, target) => {
|
|
||||||
if(!search || !target) return NULL
|
|
||||||
|
|
||||||
var preparedSearch = getPreparedSearch(search)
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
|
|
||||||
var searchBitflags = preparedSearch.bitflags
|
|
||||||
if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
|
|
||||||
|
|
||||||
return algorithm(preparedSearch, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
var go = (search, targets, options) => {
|
|
||||||
if(!search) return options?.all ? all(targets, options) : noResults
|
|
||||||
|
|
||||||
var preparedSearch = getPreparedSearch(search)
|
|
||||||
var searchBitflags = preparedSearch.bitflags
|
|
||||||
var containsSpace = preparedSearch.containsSpace
|
|
||||||
|
|
||||||
var threshold = denormalizeScore( options?.threshold || 0 )
|
|
||||||
var limit = options?.limit || INFINITY
|
|
||||||
|
|
||||||
var resultsLen = 0; var limitedCount = 0
|
|
||||||
var targetsLen = targets.length
|
|
||||||
|
|
||||||
function push_result(result) {
|
|
||||||
if(resultsLen < limit) { q.add(result); ++resultsLen }
|
|
||||||
else {
|
|
||||||
++limitedCount
|
|
||||||
if(result._score > q.peek()._score) q.replaceTop(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
|
|
||||||
|
|
||||||
// options.key
|
|
||||||
if(options?.key) {
|
|
||||||
var key = options.key
|
|
||||||
for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
|
||||||
var target = getValue(obj, key)
|
|
||||||
if(!target) continue
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
|
|
||||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
|
||||||
var result = algorithm(preparedSearch, target)
|
|
||||||
if(result === NULL) continue
|
|
||||||
if(result._score < threshold) continue
|
|
||||||
|
|
||||||
result.obj = obj
|
|
||||||
push_result(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// options.keys
|
|
||||||
} else if(options?.keys) {
|
|
||||||
var keys = options.keys
|
|
||||||
var keysLen = keys.length
|
|
||||||
|
|
||||||
outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
|
||||||
|
|
||||||
{ // early out based on bitflags
|
|
||||||
var keysBitflags = 0
|
|
||||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
|
||||||
var key = keys[keyI]
|
|
||||||
var target = getValue(obj, key)
|
|
||||||
if(!target) { tmpTargets[keyI] = noTarget; continue }
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
tmpTargets[keyI] = target
|
|
||||||
|
|
||||||
keysBitflags |= target._bitflags
|
|
||||||
}
|
|
||||||
|
|
||||||
if((searchBitflags & keysBitflags) !== searchBitflags) continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
|
|
||||||
|
|
||||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
|
||||||
target = tmpTargets[keyI]
|
|
||||||
if(target === noTarget) { tmpResults[keyI] = noTarget; continue }
|
|
||||||
|
|
||||||
tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
|
|
||||||
if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
|
|
||||||
|
|
||||||
// todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
|
|
||||||
// if our second match isn't good we ignore it instead of averaging with it
|
|
||||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {
|
|
||||||
if(allowPartialMatchScores[i] > -1000) {
|
|
||||||
if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
|
|
||||||
var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
|
|
||||||
if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(containsSpace) {
|
|
||||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
|
|
||||||
} else {
|
|
||||||
var hasAtLeast1Match = false
|
|
||||||
for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
|
|
||||||
if(!hasAtLeast1Match) continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var objResults = new KeysResult(keysLen)
|
|
||||||
for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
|
|
||||||
|
|
||||||
if(containsSpace) {
|
|
||||||
var score = 0
|
|
||||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
|
|
||||||
} else {
|
|
||||||
// todo could rewrite this scoring to be more similar to when there's spaces
|
|
||||||
// if we match multiple keys give us bonus points
|
|
||||||
var score = NEGATIVE_INFINITY
|
|
||||||
for(let i=0; i<keysLen; i++) {
|
|
||||||
var result = objResults[i]
|
|
||||||
if(result._score > -1000) {
|
|
||||||
if(score > NEGATIVE_INFINITY) {
|
|
||||||
var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
|
|
||||||
if(tmp > score) score = tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(result._score > score) score = result._score
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
objResults.obj = obj
|
|
||||||
objResults._score = score
|
|
||||||
if(options?.scoreFn) {
|
|
||||||
score = options.scoreFn(objResults)
|
|
||||||
if(!score) continue
|
|
||||||
score = denormalizeScore(score)
|
|
||||||
objResults._score = score
|
|
||||||
}
|
|
||||||
|
|
||||||
if(score < threshold) continue
|
|
||||||
push_result(objResults)
|
|
||||||
}
|
|
||||||
|
|
||||||
// no keys
|
|
||||||
} else {
|
|
||||||
for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
|
|
||||||
if(!target) continue
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
|
|
||||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
|
||||||
var result = algorithm(preparedSearch, target)
|
|
||||||
if(result === NULL) continue
|
|
||||||
if(result._score < threshold) continue
|
|
||||||
|
|
||||||
push_result(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(resultsLen === 0) return noResults
|
|
||||||
var results = new Array(resultsLen)
|
|
||||||
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
|
|
||||||
results.total = resultsLen + limitedCount
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// this is written as 1 function instead of 2 for minification. perf seems fine ...
|
|
||||||
// except when minified. the perf is very slow
|
|
||||||
var highlight = (result, open='<b>', close='</b>') => {
|
|
||||||
var callback = typeof open === 'function' ? open : undefined
|
|
||||||
|
|
||||||
var target = result.target
|
|
||||||
var targetLen = target.length
|
|
||||||
var indexes = result.indexes
|
|
||||||
var highlighted = ''
|
|
||||||
var matchI = 0
|
|
||||||
var indexesI = 0
|
|
||||||
var opened = false
|
|
||||||
var parts = []
|
|
||||||
|
|
||||||
for(var i = 0; i < targetLen; ++i) { var char = target[i]
|
|
||||||
if(indexes[indexesI] === i) {
|
|
||||||
++indexesI
|
|
||||||
if(!opened) { opened = true
|
|
||||||
if(callback) {
|
|
||||||
parts.push(highlighted); highlighted = ''
|
|
||||||
} else {
|
|
||||||
highlighted += open
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(indexesI === indexes.length) {
|
|
||||||
if(callback) {
|
|
||||||
highlighted += char
|
|
||||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
|
||||||
parts.push(target.substr(i+1))
|
|
||||||
} else {
|
|
||||||
highlighted += char + close + target.substr(i+1)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(opened) { opened = false
|
|
||||||
if(callback) {
|
|
||||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
|
||||||
} else {
|
|
||||||
highlighted += close
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
highlighted += char
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback ? parts : highlighted
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var prepare = (target) => {
|
|
||||||
if(typeof target === 'number') target = ''+target
|
|
||||||
else if(typeof target !== 'string') target = ''
|
|
||||||
var info = prepareLowerInfo(target)
|
|
||||||
return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
|
|
||||||
}
|
|
||||||
|
|
||||||
var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
|
|
||||||
|
|
||||||
|
|
||||||
// Below this point is only internal code
|
|
||||||
// Below this point is only internal code
|
|
||||||
// Below this point is only internal code
|
|
||||||
// Below this point is only internal code
|
|
||||||
|
|
||||||
|
|
||||||
class Result {
|
|
||||||
get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
|
|
||||||
set ['indexes'](indexes) { return this._indexes = indexes }
|
|
||||||
['highlight'](open, close) { return highlight(this, open, close) }
|
|
||||||
get ['score']() { return normalizeScore(this._score) }
|
|
||||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeysResult extends Array {
|
|
||||||
get ['score']() { return normalizeScore(this._score) }
|
|
||||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var new_result = (target, options) => {
|
|
||||||
const result = new Result()
|
|
||||||
result['target'] = target
|
|
||||||
result['obj'] = options.obj ?? NULL
|
|
||||||
result._score = options._score ?? NEGATIVE_INFINITY
|
|
||||||
result._indexes = options._indexes ?? []
|
|
||||||
result._targetLower = options._targetLower ?? ''
|
|
||||||
result._targetLowerCodes = options._targetLowerCodes ?? NULL
|
|
||||||
result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
|
|
||||||
result._bitflags = options._bitflags ?? 0
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var normalizeScore = score => {
|
|
||||||
if(score === NEGATIVE_INFINITY) return 0
|
|
||||||
if(score > 1) return score
|
|
||||||
return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
|
|
||||||
}
|
|
||||||
var denormalizeScore = normalizedScore => {
|
|
||||||
if(normalizedScore === 0) return NEGATIVE_INFINITY
|
|
||||||
if(normalizedScore > 1) return normalizedScore
|
|
||||||
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var prepareSearch = (search) => {
|
|
||||||
if(typeof search === 'number') search = ''+search
|
|
||||||
else if(typeof search !== 'string') search = ''
|
|
||||||
search = search.trim()
|
|
||||||
var info = prepareLowerInfo(search)
|
|
||||||
|
|
||||||
var spaceSearches = []
|
|
||||||
if(info.containsSpace) {
|
|
||||||
var searches = search.split(/\s+/)
|
|
||||||
searches = [...new Set(searches)] // distinct
|
|
||||||
for(var i=0; i<searches.length; i++) {
|
|
||||||
if(searches[i] === '') continue
|
|
||||||
var _info = prepareLowerInfo(searches[i])
|
|
||||||
spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var getPrepared = (target) => {
|
|
||||||
if(target.length > 999) return prepare(target) // don't cache huge targets
|
|
||||||
var targetPrepared = preparedCache.get(target)
|
|
||||||
if(targetPrepared !== undefined) return targetPrepared
|
|
||||||
targetPrepared = prepare(target)
|
|
||||||
preparedCache.set(target, targetPrepared)
|
|
||||||
return targetPrepared
|
|
||||||
}
|
|
||||||
var getPreparedSearch = (search) => {
|
|
||||||
if(search.length > 999) return prepareSearch(search) // don't cache huge searches
|
|
||||||
var searchPrepared = preparedSearchCache.get(search)
|
|
||||||
if(searchPrepared !== undefined) return searchPrepared
|
|
||||||
searchPrepared = prepareSearch(search)
|
|
||||||
preparedSearchCache.set(search, searchPrepared)
|
|
||||||
return searchPrepared
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var all = (targets, options) => {
|
|
||||||
var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
|
|
||||||
|
|
||||||
var limit = options?.limit || INFINITY
|
|
||||||
|
|
||||||
if(options?.key) {
|
|
||||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
|
||||||
var target = getValue(obj, options.key)
|
|
||||||
if(target == NULL) continue
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
var result = new_result(target.target, {_score: target._score, obj: obj})
|
|
||||||
results.push(result); if(results.length >= limit) return results
|
|
||||||
}
|
|
||||||
} else if(options?.keys) {
|
|
||||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
|
||||||
var objResults = new KeysResult(options.keys.length)
|
|
||||||
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
|
|
||||||
var target = getValue(obj, options.keys[keyI])
|
|
||||||
if(!target) { objResults[keyI] = noTarget; continue }
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
target._score = NEGATIVE_INFINITY
|
|
||||||
target._indexes.len = 0
|
|
||||||
objResults[keyI] = target
|
|
||||||
}
|
|
||||||
objResults.obj = obj
|
|
||||||
objResults._score = NEGATIVE_INFINITY
|
|
||||||
results.push(objResults); if(results.length >= limit) return results
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(var i=0;i<targets.length;i++) { var target = targets[i]
|
|
||||||
if(target == NULL) continue
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
target._score = NEGATIVE_INFINITY
|
|
||||||
target._indexes.len = 0
|
|
||||||
results.push(target); if(results.length >= limit) return results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
|
|
||||||
if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
|
|
||||||
|
|
||||||
var searchLower = preparedSearch._lower
|
|
||||||
var searchLowerCodes = preparedSearch.lowerCodes
|
|
||||||
var searchLowerCode = searchLowerCodes[0]
|
|
||||||
var targetLowerCodes = prepared._targetLowerCodes
|
|
||||||
var searchLen = searchLowerCodes.length
|
|
||||||
var targetLen = targetLowerCodes.length
|
|
||||||
var searchI = 0 // where we at
|
|
||||||
var targetI = 0 // where you at
|
|
||||||
var matchesSimpleLen = 0
|
|
||||||
|
|
||||||
// very basic fuzzy match; to remove non-matching targets ASAP!
|
|
||||||
// walk through target. find sequential matches.
|
|
||||||
// if all chars aren't found then exit
|
|
||||||
for(;;) {
|
|
||||||
var isMatch = searchLowerCode === targetLowerCodes[targetI]
|
|
||||||
if(isMatch) {
|
|
||||||
matchesSimple[matchesSimpleLen++] = targetI
|
|
||||||
++searchI; if(searchI === searchLen) break
|
|
||||||
searchLowerCode = searchLowerCodes[searchI]
|
|
||||||
}
|
|
||||||
++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchI = 0
|
|
||||||
var successStrict = false
|
|
||||||
var matchesStrictLen = 0
|
|
||||||
|
|
||||||
var nextBeginningIndexes = prepared._nextBeginningIndexes
|
|
||||||
if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
|
|
||||||
targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
|
|
||||||
|
|
||||||
// Our target string successfully matched all characters in sequence!
|
|
||||||
// Let's try a more advanced and strict test to improve the score
|
|
||||||
// only count it as a match if it's consecutive or a beginning character!
|
|
||||||
var backtrackCount = 0
|
|
||||||
if(targetI !== targetLen) for(;;) {
|
|
||||||
if(targetI >= targetLen) {
|
|
||||||
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
|
|
||||||
if(searchI <= 0) break // We failed to push chars forward for a better match
|
|
||||||
|
|
||||||
++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
|
|
||||||
|
|
||||||
--searchI
|
|
||||||
var lastMatch = matchesStrict[--matchesStrictLen]
|
|
||||||
targetI = nextBeginningIndexes[lastMatch]
|
|
||||||
|
|
||||||
} else {
|
|
||||||
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
|
|
||||||
if(isMatch) {
|
|
||||||
matchesStrict[matchesStrictLen++] = targetI
|
|
||||||
++searchI; if(searchI === searchLen) { successStrict = true; break }
|
|
||||||
++targetI
|
|
||||||
} else {
|
|
||||||
targetI = nextBeginningIndexes[targetI]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it's a substring match
|
|
||||||
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
|
|
||||||
var isSubstring = !!~substringIndex
|
|
||||||
var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
|
|
||||||
|
|
||||||
// if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
|
|
||||||
if(isSubstring && !isSubstringBeginning) {
|
|
||||||
for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {
|
|
||||||
if(i <= substringIndex) continue
|
|
||||||
|
|
||||||
for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break
|
|
||||||
if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tally up the score & keep track of matches for highlighting later
|
|
||||||
// if it's a simple match, we'll switch to a substring match if a substring exists
|
|
||||||
// if it's a strict match, we'll switch to a substring match only if that's a better score
|
|
||||||
|
|
||||||
var calculateScore = matches => {
|
|
||||||
var score = 0
|
|
||||||
|
|
||||||
var extraMatchGroupCount = 0
|
|
||||||
for(var i = 1; i < searchLen; ++i) {
|
|
||||||
if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
|
|
||||||
}
|
|
||||||
var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
|
|
||||||
|
|
||||||
score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
|
|
||||||
|
|
||||||
if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
|
|
||||||
|
|
||||||
if(!successStrict) {
|
|
||||||
score *= 1000
|
|
||||||
} else {
|
|
||||||
// successStrict on a target with too many beginning indexes loses points for being a bad target
|
|
||||||
var uniqueBeginningIndexes = 1
|
|
||||||
for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
|
|
||||||
|
|
||||||
if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
|
|
||||||
}
|
|
||||||
|
|
||||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
|
||||||
|
|
||||||
if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
|
|
||||||
if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
|
|
||||||
|
|
||||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
|
||||||
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!successStrict) {
|
|
||||||
if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
|
||||||
var matchesBest = matchesSimple
|
|
||||||
var score = calculateScore(matchesBest)
|
|
||||||
} else {
|
|
||||||
if(isSubstringBeginning) {
|
|
||||||
for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
|
||||||
var matchesBest = matchesSimple
|
|
||||||
var score = calculateScore(matchesSimple)
|
|
||||||
} else {
|
|
||||||
var matchesBest = matchesStrict
|
|
||||||
var score = calculateScore(matchesStrict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prepared._score = score
|
|
||||||
|
|
||||||
for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
|
|
||||||
prepared._indexes.len = searchLen
|
|
||||||
|
|
||||||
const result = new Result()
|
|
||||||
result.target = prepared.target
|
|
||||||
result._score = prepared._score
|
|
||||||
result._indexes = prepared._indexes
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
|
|
||||||
var seen_indexes = new Set()
|
|
||||||
var score = 0
|
|
||||||
var result = NULL
|
|
||||||
|
|
||||||
var first_seen_index_last_search = 0
|
|
||||||
var searches = preparedSearch.spaceSearches
|
|
||||||
var searchesLen = searches.length
|
|
||||||
var changeslen = 0
|
|
||||||
|
|
||||||
// Return _nextBeginningIndexes back to its normal state
|
|
||||||
var resetNextBeginningIndexes = () => {
|
|
||||||
for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasAtLeast1Match = false
|
|
||||||
for(var i=0; i<searchesLen; ++i) {
|
|
||||||
allowPartialMatchScores[i] = NEGATIVE_INFINITY
|
|
||||||
var search = searches[i]
|
|
||||||
|
|
||||||
result = algorithm(search, target)
|
|
||||||
if(allowPartialMatch) {
|
|
||||||
if(result === NULL) continue
|
|
||||||
hasAtLeast1Match = true
|
|
||||||
} else {
|
|
||||||
if(result === NULL) {resetNextBeginningIndexes(); return NULL}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if not the last search, we need to mutate _nextBeginningIndexes for the next search
|
|
||||||
var isTheLastSearch = i === searchesLen - 1
|
|
||||||
if(!isTheLastSearch) {
|
|
||||||
var indexes = result._indexes
|
|
||||||
|
|
||||||
var indexesIsConsecutiveSubstring = true
|
|
||||||
for(let i=0; i<indexes.len-1; i++) {
|
|
||||||
if(indexes[i+1] - indexes[i] !== 1) {
|
|
||||||
indexesIsConsecutiveSubstring = false; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(indexesIsConsecutiveSubstring) {
|
|
||||||
var newBeginningIndex = indexes[indexes.len-1] + 1
|
|
||||||
var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]
|
|
||||||
for(let i=newBeginningIndex-1; i>=0; i--) {
|
|
||||||
if(toReplace !== target._nextBeginningIndexes[i]) break
|
|
||||||
target._nextBeginningIndexes[i] = newBeginningIndex
|
|
||||||
nextBeginningIndexesChanges[changeslen*2 + 0] = i
|
|
||||||
nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
|
|
||||||
changeslen++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
score += result._score / searchesLen
|
|
||||||
allowPartialMatchScores[i] = result._score / searchesLen
|
|
||||||
|
|
||||||
// dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
|
|
||||||
if(result._indexes[0] < first_seen_index_last_search) {
|
|
||||||
score -= (first_seen_index_last_search - result._indexes[0]) * 2
|
|
||||||
}
|
|
||||||
first_seen_index_last_search = result._indexes[0]
|
|
||||||
|
|
||||||
for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
|
|
||||||
}
|
|
||||||
|
|
||||||
if(allowPartialMatch && !hasAtLeast1Match) return NULL
|
|
||||||
|
|
||||||
resetNextBeginningIndexes()
|
|
||||||
|
|
||||||
// allows a search with spaces that's an exact substring to score well
|
|
||||||
var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
|
|
||||||
if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {
|
|
||||||
if(allowPartialMatch) {
|
|
||||||
for(var i=0; i<searchesLen; ++i) {
|
|
||||||
allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allowSpacesResult
|
|
||||||
}
|
|
||||||
|
|
||||||
if(allowPartialMatch) result = target
|
|
||||||
result._score = score
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
for (let index of seen_indexes) result._indexes[i++] = index
|
|
||||||
result._indexes.len = i
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
|
|
||||||
var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
|
|
||||||
|
|
||||||
var prepareLowerInfo = (str) => {
|
|
||||||
str = remove_accents(str)
|
|
||||||
var strLen = str.length
|
|
||||||
var lower = str.toLowerCase()
|
|
||||||
var lowerCodes = [] // new Array(strLen) sparse array is too slow
|
|
||||||
var bitflags = 0
|
|
||||||
var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
|
|
||||||
|
|
||||||
for(var i = 0; i < strLen; ++i) {
|
|
||||||
var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
|
|
||||||
|
|
||||||
if(lowerCode === 32) {
|
|
||||||
containsSpace = true
|
|
||||||
continue // it's important that we don't set any bitflags for space
|
|
||||||
}
|
|
||||||
|
|
||||||
var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
|
|
||||||
: lowerCode>=48&&lowerCode<=57 ? 26 // numbers
|
|
||||||
// 3 bits available
|
|
||||||
: lowerCode<=127 ? 30 // other ascii
|
|
||||||
: 31 // other utf8
|
|
||||||
bitflags |= 1<<bit
|
|
||||||
}
|
|
||||||
|
|
||||||
return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}
|
|
||||||
}
|
|
||||||
var prepareBeginningIndexes = (target) => {
|
|
||||||
var targetLen = target.length
|
|
||||||
var beginningIndexes = []; var beginningIndexesLen = 0
|
|
||||||
var wasUpper = false
|
|
||||||
var wasAlphanum = false
|
|
||||||
for(var i = 0; i < targetLen; ++i) {
|
|
||||||
var targetCode = target.charCodeAt(i)
|
|
||||||
var isUpper = targetCode>=65&&targetCode<=90
|
|
||||||
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
|
|
||||||
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
|
|
||||||
wasUpper = isUpper
|
|
||||||
wasAlphanum = isAlphanum
|
|
||||||
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
|
|
||||||
}
|
|
||||||
return beginningIndexes
|
|
||||||
}
|
|
||||||
var prepareNextBeginningIndexes = (target) => {
|
|
||||||
target = remove_accents(target)
|
|
||||||
var targetLen = target.length
|
|
||||||
var beginningIndexes = prepareBeginningIndexes(target)
|
|
||||||
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
|
|
||||||
var lastIsBeginning = beginningIndexes[0]
|
|
||||||
var lastIsBeginningI = 0
|
|
||||||
for(var i = 0; i < targetLen; ++i) {
|
|
||||||
if(lastIsBeginning > i) {
|
|
||||||
nextBeginningIndexes[i] = lastIsBeginning
|
|
||||||
} else {
|
|
||||||
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
|
|
||||||
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nextBeginningIndexes
|
|
||||||
}
|
|
||||||
|
|
||||||
var preparedCache = new Map()
|
|
||||||
var preparedSearchCache = new Map()
|
|
||||||
|
|
||||||
// the theory behind these being globals is to reduce garbage collection by not making new arrays
|
|
||||||
var matchesSimple = []; var matchesStrict = []
|
|
||||||
var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
|
|
||||||
var keysSpacesBestScores = []; var allowPartialMatchScores = []
|
|
||||||
var tmpTargets = []; var tmpResults = []
|
|
||||||
|
|
||||||
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
|
|
||||||
// prop = 'key1.key2' 10ms
|
|
||||||
// prop = ['key1', 'key2'] 27ms
|
|
||||||
// prop = obj => obj.tags.join() ??ms
|
|
||||||
var getValue = (obj, prop) => {
|
|
||||||
var tmp = obj[prop]; if(tmp !== undefined) return tmp
|
|
||||||
if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
|
|
||||||
var segs = prop
|
|
||||||
if(!Array.isArray(prop)) segs = prop.split('.')
|
|
||||||
var len = segs.length
|
|
||||||
var i = -1
|
|
||||||
while (obj && (++i < len)) obj = obj[segs[i]]
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
|
|
||||||
var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
|
|
||||||
var noResults = []; noResults.total = 0
|
|
||||||
var NULL = null
|
|
||||||
|
|
||||||
var noTarget = prepare('')
|
|
||||||
|
|
||||||
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
|
|
||||||
var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
|
|
||||||
var q = fastpriorityqueue() // reuse this
|
|
||||||
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
|
||||||
|
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
||||||
|
@echo "Shell files installed"
|
||||||
|
|
||||||
|
install-completions:
|
||||||
|
@echo "Installing shell completions..."
|
||||||
|
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
||||||
|
@mkdir -p $(DATA_DIR)/zsh/site-functions
|
||||||
|
@mkdir -p $(DATA_DIR)/fish/vendor_completions.d
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion bash > $(DATA_DIR)/bash-completion/completions/dms 2>/dev/null || true
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion zsh > $(DATA_DIR)/zsh/site-functions/_dms 2>/dev/null || true
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion fish > $(DATA_DIR)/fish/vendor_completions.d/dms.fish 2>/dev/null || true
|
||||||
|
@echo "Shell completions installed"
|
||||||
|
|
||||||
|
install-systemd:
|
||||||
|
@echo "Installing systemd user service..."
|
||||||
|
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||||
|
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
|
||||||
|
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||||
|
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||||
|
|
||||||
|
install-icon:
|
||||||
|
@echo "Installing icon..."
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg
|
||||||
|
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||||
|
@echo "Icon installed"
|
||||||
|
|
||||||
|
install-desktop:
|
||||||
|
@echo "Installing desktop entry..."
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
|
@echo "Desktop entry installed"
|
||||||
|
|
||||||
|
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
|
@echo ""
|
||||||
|
@echo "Installation complete!"
|
||||||
|
@echo ""
|
||||||
|
@echo "=== Cheers, the DMS Team! ==="
|
||||||
|
|
||||||
|
# Uninstallation targets
|
||||||
|
uninstall-bin:
|
||||||
|
@echo "Removing $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||||
|
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Binary removed"
|
||||||
|
|
||||||
|
uninstall-shell:
|
||||||
|
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
|
||||||
|
@rm -rf $(SHELL_INSTALL_DIR)
|
||||||
|
@echo "Shell files removed"
|
||||||
|
|
||||||
|
uninstall-completions:
|
||||||
|
@echo "Removing shell completions..."
|
||||||
|
@rm -f $(DATA_DIR)/bash-completion/completions/dms
|
||||||
|
@rm -f $(DATA_DIR)/zsh/site-functions/_dms
|
||||||
|
@rm -f $(DATA_DIR)/fish/vendor_completions.d/dms.fish
|
||||||
|
@echo "Shell completions removed"
|
||||||
|
|
||||||
|
uninstall-systemd:
|
||||||
|
@echo "Removing systemd user service..."
|
||||||
|
@rm -f $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@echo "Systemd service removed"
|
||||||
|
@echo "Note: Stop/disable service manually if running: systemctl --user stop dms"
|
||||||
|
|
||||||
|
uninstall-icon:
|
||||||
|
@echo "Removing icon..."
|
||||||
|
@rm -f $(ICON_DIR)/danklogo.svg
|
||||||
|
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||||
|
@echo "Icon removed"
|
||||||
|
|
||||||
|
uninstall-desktop:
|
||||||
|
@echo "Removing desktop entry..."
|
||||||
|
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
|
@echo "Desktop entry removed"
|
||||||
|
|
||||||
|
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
|
||||||
|
@echo ""
|
||||||
|
@echo "Uninstallation complete!"
|
||||||
|
|
||||||
|
# Target assist
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo ""
|
||||||
|
@echo "Build:"
|
||||||
|
@echo " all (default) - Build the DMS binary"
|
||||||
|
@echo " build - Same as 'all'"
|
||||||
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo ""
|
||||||
|
@echo "Install:"
|
||||||
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
@echo " install-bin - Install only the binary"
|
||||||
|
@echo " install-shell - Install only shell files"
|
||||||
|
@echo " install-completions - Install only shell completions"
|
||||||
|
@echo " install-systemd - Install only systemd service"
|
||||||
|
@echo " install-icon - Install only icon"
|
||||||
|
@echo " install-desktop - Install only desktop entry"
|
||||||
|
@echo ""
|
||||||
|
@echo "Uninstall:"
|
||||||
|
@echo " uninstall - Remove everything (requires sudo)"
|
||||||
|
@echo " uninstall-bin - Remove only the binary"
|
||||||
|
@echo " uninstall-shell - Remove only shell files"
|
||||||
|
@echo " uninstall-completions - Remove only shell completions"
|
||||||
|
@echo " uninstall-systemd - Remove only systemd service"
|
||||||
|
@echo " uninstall-icon - Remove only icon"
|
||||||
|
@echo " uninstall-desktop - Remove only desktop entry"
|
||||||
|
@echo ""
|
||||||
|
@echo "Usage:"
|
||||||
|
@echo " sudo make install - Build and install DMS"
|
||||||
|
@echo " sudo make uninstall - Remove DMS"
|
||||||
|
@echo " systemctl --user enable --now dms - Enable and start service"
|
||||||
@@ -1,920 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: clipboardHistoryModal
|
|
||||||
|
|
||||||
property int totalCount: 0
|
|
||||||
property var activeTheme: Theme
|
|
||||||
property var clipboardEntries: []
|
|
||||||
property string searchText: ""
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property bool showKeyboardHints: false
|
|
||||||
property Component clipboardContent
|
|
||||||
property int activeImageLoads: 0
|
|
||||||
readonly property int maxConcurrentLoads: 3
|
|
||||||
|
|
||||||
function updateFilteredModel() {
|
|
||||||
filteredClipboardModel.clear()
|
|
||||||
for (var i = 0; i < clipboardModel.count; i++) {
|
|
||||||
const entry = clipboardModel.get(i).entry
|
|
||||||
if (searchText.trim().length === 0) {
|
|
||||||
filteredClipboardModel.append({
|
|
||||||
"entry": entry
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const content = getEntryPreview(entry).toLowerCase()
|
|
||||||
if (content.includes(searchText.toLowerCase()))
|
|
||||||
filteredClipboardModel.append({
|
|
||||||
"entry": entry
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
|
||||||
// Clamp selectedIndex to valid range
|
|
||||||
if (filteredClipboardModel.count === 0) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
|
||||||
selectedIndex = filteredClipboardModel.count - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible)
|
|
||||||
hide()
|
|
||||||
else
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
open()
|
|
||||||
clipboardHistoryModal.searchText = ""
|
|
||||||
clipboardHistoryModal.activeImageLoads = 0
|
|
||||||
|
|
||||||
initializeThumbnailSystem()
|
|
||||||
refreshClipboard()
|
|
||||||
keyboardController.reset()
|
|
||||||
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item && contentLoader.item.searchField) {
|
|
||||||
contentLoader.item.searchField.text = ""
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close()
|
|
||||||
clipboardHistoryModal.searchText = ""
|
|
||||||
clipboardHistoryModal.activeImageLoads = 0
|
|
||||||
|
|
||||||
updateFilteredModel()
|
|
||||||
keyboardController.reset()
|
|
||||||
cleanupTempFiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeThumbnailSystem() {}
|
|
||||||
|
|
||||||
function cleanupTempFiles() {
|
|
||||||
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateThumbnails() {}
|
|
||||||
|
|
||||||
function refreshClipboard() {
|
|
||||||
clipboardProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyEntry(entry) {
|
|
||||||
const entryId = entry.split('\t')[0]
|
|
||||||
Quickshell.execDetached(
|
|
||||||
["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
|
|
||||||
ToastService.showInfo("Copied to clipboard")
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteEntry(entry) {
|
|
||||||
deleteProcess.deletedEntry = entry
|
|
||||||
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(
|
|
||||||
/'/g, "'\\''")}' | cliphist delete`]
|
|
||||||
deleteProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAll() {
|
|
||||||
clearProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntryPreview(entry) {
|
|
||||||
let content = entry.replace(/^\s*\d+\s+/, "")
|
|
||||||
if (content.includes("image/") || content.includes("binary data")
|
|
||||||
|| /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
|
||||||
const dimensionMatch = content.match(/(\d+)x(\d+)/)
|
|
||||||
if (dimensionMatch)
|
|
||||||
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
|
|
||||||
|
|
||||||
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
|
|
||||||
if (typeMatch)
|
|
||||||
return `Image (${typeMatch[1].toUpperCase()})`
|
|
||||||
|
|
||||||
return "Image"
|
|
||||||
}
|
|
||||||
if (content.length > 100)
|
|
||||||
return content.substring(0, 100) + "..."
|
|
||||||
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntryType(entry) {
|
|
||||||
if (entry.includes("image/") || entry.includes("binary data")
|
|
||||||
|| /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry)
|
|
||||||
|| /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry))
|
|
||||||
return "image"
|
|
||||||
|
|
||||||
if (entry.length > 200)
|
|
||||||
return "long_text"
|
|
||||||
|
|
||||||
return "text"
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 650
|
|
||||||
height: 550
|
|
||||||
backgroundColor: Theme.popupBackground()
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
borderColor: Theme.outlineMedium
|
|
||||||
borderWidth: 1
|
|
||||||
enableShadow: true
|
|
||||||
onBackgroundClicked: {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
|
||||||
keyboardController.handleKey(event)
|
|
||||||
}
|
|
||||||
content: clipboardContent
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: keyboardController
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
selectedIndex = 0
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
showKeyboardHints = false
|
|
||||||
if (typeof clipboardListView !== 'undefined' && clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (filteredClipboardModel.count === 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1,
|
|
||||||
filteredClipboardModel.count - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (filteredClipboardModel.count === 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function copySelected() {
|
|
||||||
if (filteredClipboardModel.count === 0 || selectedIndex < 0
|
|
||||||
|| selectedIndex >= filteredClipboardModel.count)
|
|
||||||
return
|
|
||||||
|
|
||||||
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
|
|
||||||
copyEntry(selectedEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteSelected() {
|
|
||||||
if (filteredClipboardModel.count === 0 || selectedIndex < 0
|
|
||||||
|| selectedIndex >= filteredClipboardModel.count)
|
|
||||||
return
|
|
||||||
|
|
||||||
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
|
|
||||||
deleteEntry(selectedEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKey(event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
if (keyboardNavigationActive) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
if (typeof clipboardListView !== 'undefined'
|
|
||||||
&& clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = false
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
if (!keyboardNavigationActive) {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = 0
|
|
||||||
if (typeof clipboardListView !== 'undefined'
|
|
||||||
&& clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = true
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
if (!keyboardNavigationActive) {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = 0
|
|
||||||
if (typeof clipboardListView !== 'undefined'
|
|
||||||
&& clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = true
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else if (selectedIndex === 0) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
if (typeof clipboardListView !== 'undefined'
|
|
||||||
&& clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = false
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Delete
|
|
||||||
&& (event.modifiers & Qt.ShiftModifier)) {
|
|
||||||
clearAll()
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (keyboardNavigationActive) {
|
|
||||||
if ((event.key === Qt.Key_C
|
|
||||||
&& (event.modifiers & Qt.ControlModifier))
|
|
||||||
|| event.key === Qt.Key_Return
|
|
||||||
|| event.key === Qt.Key_Enter) {
|
|
||||||
copySelected()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Delete) {
|
|
||||||
deleteSelected()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.key === Qt.Key_F10) {
|
|
||||||
showKeyboardHints = !showKeyboardHints
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfirmModal {
|
|
||||||
id: clearConfirmDialog
|
|
||||||
|
|
||||||
confirmButtonText: "Clear All"
|
|
||||||
confirmButtonColor: Theme.primary
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
clipboardHistoryModal.shouldHaveFocus = false
|
|
||||||
} else if (clipboardHistoryModal.shouldBeVisible) {
|
|
||||||
clipboardHistoryModal.shouldHaveFocus = true
|
|
||||||
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
|
|
||||||
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
|
|
||||||
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: clipboardModel
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: filteredClipboardModel
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: clipboardProcess
|
|
||||||
|
|
||||||
command: ["cliphist", "list"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
clipboardModel.clear()
|
|
||||||
const lines = text.trim().split('\n')
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim().length > 0)
|
|
||||||
clipboardModel.append({
|
|
||||||
"entry": line
|
|
||||||
})
|
|
||||||
}
|
|
||||||
updateFilteredModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: deleteProcess
|
|
||||||
|
|
||||||
property string deletedEntry: ""
|
|
||||||
|
|
||||||
running: false
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
// Just remove the item from models instead of re-fetching everything
|
|
||||||
for (var i = 0; i < clipboardModel.count; i++) {
|
|
||||||
if (clipboardModel.get(
|
|
||||||
i).entry === deleteProcess.deletedEntry) {
|
|
||||||
clipboardModel.remove(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var j = 0; j < filteredClipboardModel.count; j++) {
|
|
||||||
if (filteredClipboardModel.get(
|
|
||||||
j).entry === deleteProcess.deletedEntry) {
|
|
||||||
filteredClipboardModel.remove(j)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
|
||||||
// Clamp selectedIndex to valid range
|
|
||||||
if (filteredClipboardModel.count === 0) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
|
||||||
selectedIndex = filteredClipboardModel.count - 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Failed to delete clipboard entry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: clearProcess
|
|
||||||
|
|
||||||
command: ["cliphist", "wipe"]
|
|
||||||
running: false
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
clipboardModel.clear()
|
|
||||||
filteredClipboardModel.clear()
|
|
||||||
totalCount = 0
|
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open() {
|
|
||||||
clipboardHistoryModal.show()
|
|
||||||
return "CLIPBOARD_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
hide()
|
|
||||||
return "CLIPBOARD_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
clipboardHistoryModal.toggle()
|
|
||||||
return "CLIPBOARD_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "clipboard"
|
|
||||||
}
|
|
||||||
|
|
||||||
clipboardContent: Component {
|
|
||||||
Item {
|
|
||||||
id: clipboardContent
|
|
||||||
property alias searchField: searchField
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
focus: false
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "content_paste"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `Clipboard History (${totalCount})`
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "info"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
|
||||||
hoverColor: Theme.primaryHover
|
|
||||||
onClicked: {
|
|
||||||
showKeyboardHints = !showKeyboardHints
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "delete_sweep"
|
|
||||||
iconSize: Theme.iconSize
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.surfaceHover
|
|
||||||
onClicked: {
|
|
||||||
clearConfirmDialog.show(
|
|
||||||
"Clear All History?",
|
|
||||||
"This will permanently delete all clipboard history.",
|
|
||||||
function() {
|
|
||||||
clearAll()
|
|
||||||
hide()
|
|
||||||
},
|
|
||||||
function() {} // No action on cancel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.surfaceHover
|
|
||||||
onClicked: hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: ""
|
|
||||||
leftIconName: "search"
|
|
||||||
showClearButton: true
|
|
||||||
focus: true
|
|
||||||
ignoreLeftRightKeys: true
|
|
||||||
keyForwardTargets: [modalFocusScope]
|
|
||||||
onTextChanged: {
|
|
||||||
clipboardHistoryModal.searchText = text
|
|
||||||
updateFilteredModel()
|
|
||||||
}
|
|
||||||
Keys.onEscapePressed: function (event) {
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: clipboardHistoryModal
|
|
||||||
function onOpened() {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
searchField.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 110
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: clipboardListView
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
var itemHeight = 72 + spacing
|
|
||||||
var itemY = index * itemHeight
|
|
||||||
var itemBottom = itemY + itemHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
clip: true
|
|
||||||
model: filteredClipboardModel
|
|
||||||
currentIndex: selectedIndex
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
interactive: true
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
|
||||||
pressDelay: 0
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive && currentIndex >= 0)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No clipboard entries found"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: filteredClipboardModel.count === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
property string entryType: getEntryType(model.entry)
|
|
||||||
property string entryPreview: getEntryPreview(
|
|
||||||
model.entry)
|
|
||||||
property int entryIndex: index + 1
|
|
||||||
property string entryData: model.entry
|
|
||||||
property alias thumbnailImageSource: thumbnailImageSource
|
|
||||||
|
|
||||||
width: clipboardListView.width
|
|
||||||
height: 72
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (keyboardNavigationActive
|
|
||||||
&& index === selectedIndex)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.2)
|
|
||||||
|
|
||||||
return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
if (keyboardNavigationActive
|
|
||||||
&& index === selectedIndex)
|
|
||||||
return Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.5)
|
|
||||||
|
|
||||||
return Theme.outlineStrong
|
|
||||||
}
|
|
||||||
border.width: keyboardNavigationActive
|
|
||||||
&& index === selectedIndex ? 1.5 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
radius: 12
|
|
||||||
color: Theme.primarySelected
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: entryIndex.toString()
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - 68
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: entryType === "image" ? 48 : Theme.iconSize
|
|
||||||
height: entryType === "image" ? 48 : Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: thumbnailImageSource
|
|
||||||
|
|
||||||
property string entryId: model.entry.split(
|
|
||||||
'\t')[0]
|
|
||||||
property bool isVisible: false
|
|
||||||
property string cachedImageData: ""
|
|
||||||
property bool loadQueued: false
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: ""
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
smooth: true
|
|
||||||
cache: false // Disable Qt's cache to control it ourselves
|
|
||||||
visible: false
|
|
||||||
asynchronous: true
|
|
||||||
sourceSize.width: 128
|
|
||||||
sourceSize.height: 128
|
|
||||||
|
|
||||||
onCachedImageDataChanged: {
|
|
||||||
if (cachedImageData) {
|
|
||||||
source = ""
|
|
||||||
source = `data:image/png;base64,${cachedImageData}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryLoadImage() {
|
|
||||||
if (!loadQueued && entryType === "image" && !cachedImageData) {
|
|
||||||
loadQueued = true
|
|
||||||
if (clipboardHistoryModal.activeImageLoads < clipboardHistoryModal.maxConcurrentLoads) {
|
|
||||||
clipboardHistoryModal.activeImageLoads++
|
|
||||||
imageLoader.running = true
|
|
||||||
} else {
|
|
||||||
// Retry after delay
|
|
||||||
retryTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: retryTimer
|
|
||||||
interval: 50
|
|
||||||
onTriggered: {
|
|
||||||
if (thumbnailImageSource.loadQueued && !imageLoader.running) {
|
|
||||||
if (clipboardHistoryModal.activeImageLoads < clipboardHistoryModal.maxConcurrentLoads) {
|
|
||||||
clipboardHistoryModal.activeImageLoads++
|
|
||||||
imageLoader.running = true
|
|
||||||
} else {
|
|
||||||
retryTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (entryType !== "image") return
|
|
||||||
|
|
||||||
// Check if item is visible on screen initially
|
|
||||||
let itemY = index * (72 + clipboardListView.spacing)
|
|
||||||
let viewTop = clipboardListView.contentY
|
|
||||||
let viewBottom = viewTop + clipboardListView.height
|
|
||||||
isVisible = (itemY + 72 >= viewTop && itemY <= viewBottom)
|
|
||||||
|
|
||||||
if (isVisible) {
|
|
||||||
tryLoadImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: clipboardListView
|
|
||||||
function onContentYChanged() {
|
|
||||||
if (entryType !== "image") return
|
|
||||||
|
|
||||||
let itemY = index * (72 + clipboardListView.spacing)
|
|
||||||
let viewTop = clipboardListView.contentY - 100 // Preload slightly before visible
|
|
||||||
let viewBottom = viewTop + clipboardListView.height + 200
|
|
||||||
let nowVisible = (itemY + 72 >= viewTop && itemY <= viewBottom)
|
|
||||||
|
|
||||||
if (nowVisible && !thumbnailImageSource.isVisible) {
|
|
||||||
thumbnailImageSource.isVisible = true
|
|
||||||
thumbnailImageSource.tryLoadImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: imageLoader
|
|
||||||
|
|
||||||
running: false
|
|
||||||
|
|
||||||
command: ["sh", "-c", `cliphist decode ${thumbnailImageSource.entryId} | base64 -w 0`]
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
let imageData = text.trim()
|
|
||||||
if (imageData && imageData.length > 0) {
|
|
||||||
thumbnailImageSource.cachedImageData = imageData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
thumbnailImageSource.loadQueued = false
|
|
||||||
if (clipboardHistoryModal.activeImageLoads > 0) {
|
|
||||||
clipboardHistoryModal.activeImageLoads--
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("Failed to load clipboard image:", thumbnailImageSource.entryId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
source: thumbnailImageSource
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: clipboardCircularMask
|
|
||||||
visible: entryType === "image"
|
|
||||||
&& thumbnailImageSource.status === Image.Ready
|
|
||||||
&& thumbnailImageSource.source != ""
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: clipboardCircularMask
|
|
||||||
|
|
||||||
width: 48 - 4
|
|
||||||
height: 48 - 4
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
visible: !(entryType === "image"
|
|
||||||
&& thumbnailImageSource.status === Image.Ready
|
|
||||||
&& thumbnailImageSource.source != "")
|
|
||||||
name: {
|
|
||||||
if (entryType === "image")
|
|
||||||
return "image"
|
|
||||||
|
|
||||||
if (entryType === "long_text")
|
|
||||||
return "subject"
|
|
||||||
|
|
||||||
return "content_copy"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - (entryType === "image" ? 48 : Theme.iconSize) - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
switch (entryType) {
|
|
||||||
case "image":
|
|
||||||
return "Image • " + entryPreview
|
|
||||||
case "long_text":
|
|
||||||
return "Long Text"
|
|
||||||
default:
|
|
||||||
return "Text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: contentText
|
|
||||||
|
|
||||||
text: entryPreview
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
maximumLineCount: entryType === "long_text" ? 3 : 1
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 6
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.surfaceHover
|
|
||||||
onClicked: {
|
|
||||||
deleteEntry(model.entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 40
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: copyEntry(model.entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: showKeyboardHints ? 80 + Theme.spacingL : 0
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
|
||||||
Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.95)
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 2
|
|
||||||
opacity: showKeyboardHints ? 1 : 0
|
|
||||||
z: 100
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Shift+Del: Clear All • Esc: Close"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string confirmTitle: ""
|
|
||||||
property string confirmMessage: ""
|
|
||||||
property string confirmButtonText: "Confirm"
|
|
||||||
property string cancelButtonText: "Cancel"
|
|
||||||
property color confirmButtonColor: Theme.primary
|
|
||||||
property var onConfirm: function() {}
|
|
||||||
property var onCancel: function() {}
|
|
||||||
|
|
||||||
property int selectedButton: -1 // -1 = none, 0 = Cancel, 1 = Confirm
|
|
||||||
property bool keyboardNavigation: false
|
|
||||||
|
|
||||||
function show(title, message, onConfirmCallback, onCancelCallback) {
|
|
||||||
confirmTitle = title || ""
|
|
||||||
confirmMessage = message || ""
|
|
||||||
confirmButtonText = "Confirm"
|
|
||||||
cancelButtonText = "Cancel"
|
|
||||||
confirmButtonColor = Theme.primary
|
|
||||||
onConfirm = onConfirmCallback || function() {}
|
|
||||||
onCancel = onCancelCallback || function() {}
|
|
||||||
selectedButton = -1
|
|
||||||
keyboardNavigation = false
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWithOptions(options) {
|
|
||||||
confirmTitle = options.title || ""
|
|
||||||
confirmMessage = options.message || ""
|
|
||||||
confirmButtonText = options.confirmText || "Confirm"
|
|
||||||
cancelButtonText = options.cancelText || "Cancel"
|
|
||||||
confirmButtonColor = options.confirmColor || Theme.primary
|
|
||||||
onConfirm = options.onConfirm || function() {}
|
|
||||||
onCancel = options.onCancel || function() {}
|
|
||||||
selectedButton = -1
|
|
||||||
keyboardNavigation = false
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectButton() {
|
|
||||||
if (selectedButton === 0) {
|
|
||||||
close()
|
|
||||||
if (onCancel) onCancel()
|
|
||||||
} else {
|
|
||||||
close()
|
|
||||||
if (onConfirm) onConfirm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
allowStacking: true
|
|
||||||
width: 350
|
|
||||||
height: 160
|
|
||||||
enableShadow: true
|
|
||||||
shouldHaveFocus: true
|
|
||||||
onBackgroundClicked: {
|
|
||||||
close()
|
|
||||||
if (onCancel) onCancel()
|
|
||||||
}
|
|
||||||
onOpened: {
|
|
||||||
modalFocusScope.forceActiveFocus()
|
|
||||||
modalFocusScope.focus = true
|
|
||||||
shouldHaveFocus = true
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: function(event) {
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Escape:
|
|
||||||
close()
|
|
||||||
if (onCancel) onCancel()
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Left:
|
|
||||||
case Qt.Key_Up:
|
|
||||||
keyboardNavigation = true
|
|
||||||
selectedButton = 0
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Right:
|
|
||||||
case Qt.Key_Down:
|
|
||||||
keyboardNavigation = true
|
|
||||||
selectedButton = 1
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
keyboardNavigation = true
|
|
||||||
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
if (selectedButton !== -1) {
|
|
||||||
selectButton()
|
|
||||||
} else {
|
|
||||||
selectedButton = 1
|
|
||||||
selectButton()
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: confirmTitle
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: confirmMessage
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
height: Theme.spacingS
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 120
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (keyboardNavigation && selectedButton === 0)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
else if (cancelButton.containsMouse)
|
|
||||||
return Theme.surfacePressed
|
|
||||||
else
|
|
||||||
return Theme.surfaceVariantAlpha
|
|
||||||
}
|
|
||||||
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
|
|
||||||
border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: cancelButtonText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cancelButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
selectedButton = 0
|
|
||||||
selectButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 120
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
let baseColor = confirmButtonColor
|
|
||||||
if (keyboardNavigation && selectedButton === 1)
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
|
|
||||||
else if (confirmButton.containsMouse)
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
|
|
||||||
else
|
|
||||||
return baseColor
|
|
||||||
}
|
|
||||||
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
|
|
||||||
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: confirmButtonText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primaryText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: confirmButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
selectedButton = 1
|
|
||||||
selectButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,241 +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 real width: 400
|
|
||||||
property real height: 300
|
|
||||||
readonly property real screenWidth: screen ? screen.width : 1920
|
|
||||||
readonly property real screenHeight: screen ? screen.height : 1080
|
|
||||||
property bool showBackground: true
|
|
||||||
property real backgroundOpacity: 0.5
|
|
||||||
property string positioning: "center"
|
|
||||||
property point customPosition: Qt.point(0, 0)
|
|
||||||
property bool closeOnEscapeKey: true
|
|
||||||
property bool closeOnBackgroundClick: true
|
|
||||||
property string animationType: "scale"
|
|
||||||
property int animationDuration: Theme.shorterDuration
|
|
||||||
property var animationEasing: Theme.emphasizedEasing
|
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
|
||||||
property color borderColor: Theme.outlineMedium
|
|
||||||
property real borderWidth: 1
|
|
||||||
property real cornerRadius: Theme.cornerRadius
|
|
||||||
property bool enableShadow: false
|
|
||||||
// Expose the focusScope for external access
|
|
||||||
property alias modalFocusScope: focusScope
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property bool shouldHaveFocus: shouldBeVisible
|
|
||||||
property bool allowFocusOverride: false
|
|
||||||
property bool allowStacking: false
|
|
||||||
|
|
||||||
signal opened
|
|
||||||
signal dialogClosed
|
|
||||||
signal backgroundClicked
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: ModalManager
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Overlay
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
|
|
||||||
interval: animationDuration + 50
|
|
||||||
onTriggered: {
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: background
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "black"
|
|
||||||
opacity: root.showBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
|
||||||
visible: root.showBackground
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.closeOnBackgroundClick
|
|
||||||
onClicked: mouse => {
|
|
||||||
var localPos = mapToItem(contentContainer,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
if (localPos.x < 0
|
|
||||||
|| localPos.x > contentContainer.width
|
|
||||||
|| localPos.y < 0
|
|
||||||
|| localPos.y > contentContainer.height)
|
|
||||||
root.backgroundClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: root.animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contentContainer
|
|
||||||
|
|
||||||
width: root.width
|
|
||||||
height: root.height
|
|
||||||
anchors.centerIn: positioning === "center" ? parent : undefined
|
|
||||||
x: {
|
|
||||||
if (positioning === "top-right")
|
|
||||||
return Math.max(Theme.spacingL,
|
|
||||||
root.screenWidth - width - Theme.spacingL)
|
|
||||||
else if (positioning === "custom")
|
|
||||||
return root.customPosition.x
|
|
||||||
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (positioning === "top-right")
|
|
||||||
return Theme.barHeight + Theme.spacingXS
|
|
||||||
else if (positioning === "custom")
|
|
||||||
return root.customPosition.y
|
|
||||||
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
|
|
||||||
}
|
|
||||||
color: root.backgroundColor
|
|
||||||
radius: root.cornerRadius
|
|
||||||
border.color: root.borderColor
|
|
||||||
border.width: root.borderWidth
|
|
||||||
layer.enabled: root.enableShadow
|
|
||||||
opacity: root.shouldBeVisible ? 1 : 0
|
|
||||||
scale: {
|
|
||||||
if (root.animationType === "scale")
|
|
||||||
return root.shouldBeVisible ? 1 : 0.9
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
transform: root.animationType === "slide" ? slideTransform : null
|
|
||||||
|
|
||||||
Translate {
|
|
||||||
id: slideTransform
|
|
||||||
|
|
||||||
x: root.shouldBeVisible ? 0 : 15
|
|
||||||
y: root.shouldBeVisible ? 0 : -30
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.visible
|
|
||||||
asynchronous: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: root.animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
enabled: root.animationType === "scale"
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: root.animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 8
|
|
||||||
shadowBlur: 1
|
|
||||||
shadowColor: Theme.shadowStrong
|
|
||||||
shadowOpacity: 0.3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: focusScope
|
|
||||||
|
|
||||||
objectName: "modalFocusScope"
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.visible // Only active when the modal is visible
|
|
||||||
focus: root.visible
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
if (root.closeOnEscapeKey
|
|
||||||
&& shouldHaveFocus) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && shouldHaveFocus)
|
|
||||||
Qt.callLater(function () {
|
|
||||||
focusScope.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onShouldHaveFocusChanged() {
|
|
||||||
if (shouldHaveFocus && visible) {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
focusScope.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,727 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtCore
|
|
||||||
import Qt.labs.folderlistmodel
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: fileBrowserModal
|
|
||||||
objectName: "fileBrowserModal"
|
|
||||||
allowStacking: true
|
|
||||||
|
|
||||||
signal fileSelected(string path)
|
|
||||||
|
|
||||||
property string homeDir: StandardPaths.writableLocation(
|
|
||||||
StandardPaths.HomeLocation)
|
|
||||||
property string currentPath: ""
|
|
||||||
property var fileExtensions: ["*.*"]
|
|
||||||
property alias filterExtensions: fileBrowserModal.fileExtensions
|
|
||||||
property string browserTitle: "Select File"
|
|
||||||
property string browserIcon: "folder_open"
|
|
||||||
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
|
|
||||||
property bool showHiddenFiles: false
|
|
||||||
property int selectedIndex: -1
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property bool backButtonFocused: false
|
|
||||||
property bool saveMode: false // Enable save functionality
|
|
||||||
property string defaultFileName: "" // Default filename for save mode
|
|
||||||
|
|
||||||
FolderListModel {
|
|
||||||
id: folderModel
|
|
||||||
showDirsFirst: true
|
|
||||||
showDotAndDotDot: false
|
|
||||||
showHidden: fileBrowserModal.showHiddenFiles
|
|
||||||
nameFilters: fileExtensions
|
|
||||||
showFiles: true
|
|
||||||
showDirs: true
|
|
||||||
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function isImageFile(fileName) {
|
|
||||||
if (!fileName)
|
|
||||||
return false
|
|
||||||
var ext = fileName.toLowerCase().split('.').pop()
|
|
||||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastPath() {
|
|
||||||
var lastPath = ""
|
|
||||||
if (browserType === "wallpaper") {
|
|
||||||
lastPath = SessionData.wallpaperLastPath
|
|
||||||
} else if (browserType === "profile") {
|
|
||||||
lastPath = SessionData.profileLastPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastPath && lastPath !== "") {
|
|
||||||
return lastPath
|
|
||||||
}
|
|
||||||
return homeDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveLastPath(path) {
|
|
||||||
if (browserType === "wallpaper") {
|
|
||||||
SessionData.setWallpaperLastPath(path)
|
|
||||||
} else if (browserType === "profile") {
|
|
||||||
SessionData.setProfileLastPath(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
currentPath = getLastPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 800
|
|
||||||
height: 600
|
|
||||||
enableShadow: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
onBackgroundClicked: close()
|
|
||||||
|
|
||||||
onOpened: {
|
|
||||||
modalFocusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
modalFocusScope.Keys.onPressed: function(event) {
|
|
||||||
keyboardController.handleKey(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
var startPath = getLastPath()
|
|
||||||
currentPath = startPath
|
|
||||||
selectedIndex = -1
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
backButtonFocused = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentPathChanged: {
|
|
||||||
selectedFilePath = ""
|
|
||||||
selectedFileName = ""
|
|
||||||
selectedFileIsDir = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelectedIndexChanged: {
|
|
||||||
// Update selected file data when index changes
|
|
||||||
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
|
||||||
// We need to get the file data from the model for this index
|
|
||||||
// This is a bit tricky with FolderListModel, so we'll use a different approach
|
|
||||||
selectedFilePath = ""
|
|
||||||
selectedFileName = ""
|
|
||||||
selectedFileIsDir = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to update file data from delegates
|
|
||||||
function setSelectedFileData(path, name, isDir) {
|
|
||||||
selectedFilePath = path
|
|
||||||
selectedFileName = name
|
|
||||||
selectedFileIsDir = isDir
|
|
||||||
}
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: keyboardController
|
|
||||||
|
|
||||||
property int totalItems: folderModel.count
|
|
||||||
property int gridColumns: 5 // Fixed number of columns for the grid (matches actual display)
|
|
||||||
|
|
||||||
function handleKey(event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
close()
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// F10 toggles keyboard hints
|
|
||||||
if (event.key === Qt.Key_F10) {
|
|
||||||
showKeyboardHints = !showKeyboardHints
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// F1 or I key for file information
|
|
||||||
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
|
||||||
showFileInfo = !showFileInfo
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alt+Left or Backspace to go back
|
|
||||||
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
|
||||||
if (currentPath !== homeDir) {
|
|
||||||
navigateUp()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keyboardNavigationActive) {
|
|
||||||
if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right) {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
} else {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
} else {
|
|
||||||
selectedIndex = 0
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
|
|
||||||
case Qt.Key_Backtab:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = totalItems - 1
|
|
||||||
} else if (selectedIndex > 0) {
|
|
||||||
selectedIndex--
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
} else {
|
|
||||||
selectedIndex = totalItems - 1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
|
|
||||||
case Qt.Key_Left:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (selectedIndex > 0) {
|
|
||||||
selectedIndex--
|
|
||||||
// Update file info for navigation
|
|
||||||
updateFileInfoForIndex(selectedIndex)
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
|
|
||||||
case Qt.Key_Right:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
updateFileInfoForIndex(selectedIndex)
|
|
||||||
} else if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
updateFileInfoForIndex(selectedIndex)
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
|
|
||||||
case Qt.Key_Up:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
// Go to first row, appropriate column
|
|
||||||
var col = selectedIndex % gridColumns
|
|
||||||
selectedIndex = Math.min(col, totalItems - 1)
|
|
||||||
updateFileInfoForIndex(selectedIndex)
|
|
||||||
} else if (selectedIndex >= gridColumns) {
|
|
||||||
// Move up one row
|
|
||||||
selectedIndex -= gridColumns
|
|
||||||
updateFileInfoForIndex(selectedIndex)
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
// At top row, go to back button
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
|
|
||||||
case Qt.Key_Down:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
updateFileInfoForIndex(selectedIndex)
|
|
||||||
} else {
|
|
||||||
// Move down one row if possible
|
|
||||||
var newIndex = selectedIndex + gridColumns
|
|
||||||
if (newIndex < totalItems) {
|
|
||||||
selectedIndex = newIndex
|
|
||||||
updateFileInfoForIndex(selectedIndex)
|
|
||||||
} else {
|
|
||||||
// If can't go down a full row, go to last item in the column if exists
|
|
||||||
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
|
|
||||||
var col = selectedIndex % gridColumns
|
|
||||||
var targetIndex = lastRowStart + col
|
|
||||||
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
|
||||||
selectedIndex = targetIndex
|
|
||||||
updateFileInfoForIndex(selectedIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
case Qt.Key_Space:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
navigateUp()
|
|
||||||
} else if (selectedIndex >= 0 && selectedIndex < totalItems) {
|
|
||||||
// Trigger selection by setting the grid's current index and using signal
|
|
||||||
fileBrowserModal.keyboardFileSelection(selectedIndex)
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll handling is done in the grid's onCurrentIndexChanged
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateUp() {
|
|
||||||
var path = currentPath
|
|
||||||
|
|
||||||
if (path === homeDir) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastSlash = path.lastIndexOf('/')
|
|
||||||
if (lastSlash > 0) {
|
|
||||||
var newPath = path.substring(0, lastSlash)
|
|
||||||
if (newPath.length < homeDir.length) {
|
|
||||||
currentPath = homeDir
|
|
||||||
saveLastPath(homeDir)
|
|
||||||
} else {
|
|
||||||
currentPath = newPath
|
|
||||||
saveLastPath(newPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateTo(path) {
|
|
||||||
currentPath = path
|
|
||||||
saveLastPath(path) // Save the path when navigating
|
|
||||||
selectedIndex = -1
|
|
||||||
backButtonFocused = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyboardFileSelection(index) {
|
|
||||||
if (index >= 0) {
|
|
||||||
keyboardSelectionTimer.targetIndex = index
|
|
||||||
keyboardSelectionTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSelectedFileInfo(index) {
|
|
||||||
// This will be called when we need to update file info for the selected index
|
|
||||||
// The delegate will handle the actual file info updates
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFileInfoForIndex(index) {
|
|
||||||
// We can't directly access FolderListModel data by index from here
|
|
||||||
// Instead, we'll rely on the delegate's Component.onCompleted and mouse clicks
|
|
||||||
// to call setSelectedFileData() with the proper file information
|
|
||||||
|
|
||||||
// For keyboard navigation, we need a different approach
|
|
||||||
// The selectedIndex change will trigger delegate updates
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: keyboardSelectionTimer
|
|
||||||
interval: 1
|
|
||||||
property int targetIndex: -1
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
// Access the currently selected item through model role names
|
|
||||||
// This will work because QML models expose role data
|
|
||||||
executeKeyboardSelection(targetIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeKeyboardSelection(index) {
|
|
||||||
// This is a simplified version that just needs to work
|
|
||||||
// We'll handle this in the mouse area of each delegate
|
|
||||||
// For now, signal that keyboard selection was requested
|
|
||||||
keyboardSelectionIndex = index
|
|
||||||
keyboardSelectionRequested = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
property int keyboardSelectionIndex: -1
|
|
||||||
property bool keyboardSelectionRequested: false
|
|
||||||
property bool showKeyboardHints: false
|
|
||||||
property bool showFileInfo: false
|
|
||||||
property string selectedFilePath: ""
|
|
||||||
property string selectedFileName: ""
|
|
||||||
property bool selectedFileIsDir: false
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: browserIcon
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: browserTitle
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "help"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.surfacePressed
|
|
||||||
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive))
|
|
||||||
&& currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
|
|
||||||
opacity: currentPath !== homeDir ? 1.0 : 0.0
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "arrow_back"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: backButtonMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: currentPath !== homeDir
|
|
||||||
cursorShape: currentPath
|
|
||||||
!== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: currentPath !== homeDir
|
|
||||||
onClicked: navigateUp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: fileBrowserModal.currentPath.replace("file://", "")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
width: parent.width - 40 - Theme.spacingS
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
maximumLineCount: 1
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: fileGrid
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 80
|
|
||||||
clip: true
|
|
||||||
cellWidth: 150
|
|
||||||
cellHeight: 130
|
|
||||||
cacheBuffer: 260
|
|
||||||
|
|
||||||
model: folderModel
|
|
||||||
currentIndex: selectedIndex
|
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive && currentIndex >= 0) {
|
|
||||||
positionViewAtIndex(currentIndex, GridView.Contain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: StyledRect {
|
|
||||||
id: delegateRoot
|
|
||||||
|
|
||||||
required property bool fileIsDir
|
|
||||||
required property string filePath
|
|
||||||
required property string fileName
|
|
||||||
required property url fileURL
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: 140
|
|
||||||
height: 120
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex) {
|
|
||||||
return Theme.surfacePressed
|
|
||||||
}
|
|
||||||
return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
||||||
}
|
|
||||||
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline
|
|
||||||
border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0
|
|
||||||
|
|
||||||
// Update file info when this item gets selected via keyboard or initially
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex) {
|
|
||||||
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for selectedIndex changes to update file info during keyboard navigation
|
|
||||||
Connections {
|
|
||||||
target: fileBrowserModal
|
|
||||||
function onSelectedIndexChanged() {
|
|
||||||
if (keyboardNavigationActive && selectedIndex === delegateRoot.index) {
|
|
||||||
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 80
|
|
||||||
height: 60
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
CachingImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
visible: !delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)
|
|
||||||
maxCacheSize: 80
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "description"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
visible: !delegateRoot.fileIsDir
|
|
||||||
&& !isImageFile(delegateRoot.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "folder"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
visible: delegateRoot.fileIsDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: delegateRoot.fileName || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: 120
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
maximumLineCount: 2
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
// Update selected file info and index first
|
|
||||||
selectedIndex = delegateRoot.index
|
|
||||||
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
|
||||||
|
|
||||||
if (delegateRoot.fileIsDir) {
|
|
||||||
navigateTo(delegateRoot.filePath)
|
|
||||||
} else {
|
|
||||||
fileSelected(delegateRoot.filePath)
|
|
||||||
fileBrowserModal.close() // Close modal after file selection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle keyboard selection
|
|
||||||
Connections {
|
|
||||||
target: fileBrowserModal
|
|
||||||
function onKeyboardSelectionRequestedChanged() {
|
|
||||||
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) {
|
|
||||||
// Reset the flag first
|
|
||||||
fileBrowserModal.keyboardSelectionRequested = false
|
|
||||||
// Update selected file info and index first
|
|
||||||
selectedIndex = delegateRoot.index
|
|
||||||
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
|
||||||
// Trigger the same action as mouse click
|
|
||||||
if (delegateRoot.fileIsDir) {
|
|
||||||
navigateTo(delegateRoot.filePath)
|
|
||||||
} else {
|
|
||||||
fileSelected(delegateRoot.filePath)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save functionality - positioned at bottom in save mode
|
|
||||||
Row {
|
|
||||||
id: saveRow
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
height: saveMode ? 40 : 0
|
|
||||||
visible: saveMode
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: fileNameInput
|
|
||||||
width: parent.width - saveButton.width - Theme.spacingM
|
|
||||||
height: 36
|
|
||||||
text: defaultFileName
|
|
||||||
placeholderText: "Enter filename..."
|
|
||||||
ignoreLeftRightKeys: false // Allow arrow key navigation
|
|
||||||
focus: saveMode // Auto-focus when in save mode
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (saveMode) {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onAccepted: {
|
|
||||||
if (text.trim() !== "") {
|
|
||||||
var fullPath = currentPath + "/" + text.trim();
|
|
||||||
fileSelected(fullPath);
|
|
||||||
fileBrowserModal.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
id: saveButton
|
|
||||||
width: 80
|
|
||||||
height: 36
|
|
||||||
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Save"
|
|
||||||
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
stateColor: Theme.primary
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
enabled: fileNameInput.text.trim() !== ""
|
|
||||||
onClicked: {
|
|
||||||
if (fileNameInput.text.trim() !== "") {
|
|
||||||
var fullPath = currentPath + "/" + fileNameInput.text.trim();
|
|
||||||
fileSelected(fullPath);
|
|
||||||
fileBrowserModal.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserKeyboardHints {
|
|
||||||
id: keyboardHints
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
showHints: fileBrowserModal.showKeyboardHints
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserFileInfo {
|
|
||||||
id: fileInfo
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
width: 300
|
|
||||||
showFileInfo: fileBrowserModal.showFileInfo
|
|
||||||
selectedIndex: fileBrowserModal.selectedIndex
|
|
||||||
sourceFolderModel: folderModel
|
|
||||||
currentPath: fileBrowserModal.currentPath
|
|
||||||
|
|
||||||
// Bind directly to the modal's selected file properties
|
|
||||||
currentFileName: fileBrowserModal.selectedFileName
|
|
||||||
currentFileIsDir: fileBrowserModal.selectedFileIsDir
|
|
||||||
currentFileExtension: {
|
|
||||||
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName) return ""
|
|
||||||
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
|
|
||||||
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool notepadModalVisible: false
|
|
||||||
property bool fileDialogOpen: false
|
|
||||||
property string currentFileName: "" // Track the currently loaded file
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
notepadModalVisible = true;
|
|
||||||
shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return notepadModalVisible && !fileDialogOpen;
|
|
||||||
});
|
|
||||||
open();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
notepadModalVisible = false;
|
|
||||||
// Clear filename when closing (so it doesn't persist between sessions)
|
|
||||||
currentFileName = "";
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (notepadModalVisible)
|
|
||||||
hide();
|
|
||||||
else
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: notepadModalVisible
|
|
||||||
width: 700
|
|
||||||
height: 500
|
|
||||||
enableShadow: true
|
|
||||||
onShouldHaveFocusChanged: {
|
|
||||||
console.log("Notepad: shouldHaveFocus changed to", shouldHaveFocus, "modalVisible:", notepadModalVisible, "dialogOpen:", fileDialogOpen);
|
|
||||||
}
|
|
||||||
onBackgroundClicked: hide()
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
id: contentItem
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
property alias textArea: textArea
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onNotepadModalVisibleChanged() {
|
|
||||||
if (root.notepadModalVisible) {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
textArea.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width - closeButton.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Notepad"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: currentFileName
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
visible: currentFileName !== ""
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
maximumLineCount: 1
|
|
||||||
width: 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: SessionData.notepadContent.length > 0 ? `${SessionData.notepadContent.length} characters` : "Empty"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: closeButton
|
|
||||||
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 80
|
|
||||||
color: Theme.surface
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
id: scrollView
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 1
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
TextArea {
|
|
||||||
id: textArea
|
|
||||||
|
|
||||||
text: SessionData.notepadContent
|
|
||||||
placeholderText: "Start typing your notes here..."
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
selectByMouse: true
|
|
||||||
selectByKeyboard: true
|
|
||||||
wrapMode: TextArea.Wrap
|
|
||||||
focus: root.notepadModalVisible
|
|
||||||
activeFocusOnTab: true
|
|
||||||
|
|
||||||
onTextChanged: {
|
|
||||||
if (text !== SessionData.notepadContent) {
|
|
||||||
SessionData.notepadContent = text;
|
|
||||||
saveTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onEscapePressed: (event) => {
|
|
||||||
root.hide();
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.notepadModalVisible) {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "save"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.primary
|
|
||||||
hoverColor: Theme.primaryHover
|
|
||||||
onClicked: {
|
|
||||||
console.log("Notepad: Opening save dialog, releasing modal focus");
|
|
||||||
root.allowFocusOverride = true;
|
|
||||||
root.shouldHaveFocus = false;
|
|
||||||
fileDialogOpen = true;
|
|
||||||
saveBrowser.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "Save to file"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "folder_open"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.secondary
|
|
||||||
hoverColor: Theme.secondaryHover
|
|
||||||
onClicked: {
|
|
||||||
console.log("Notepad: Opening load dialog, releasing modal focus");
|
|
||||||
root.allowFocusOverride = true;
|
|
||||||
root.shouldHaveFocus = false;
|
|
||||||
fileDialogOpen = true;
|
|
||||||
loadBrowser.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "Load file"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 1
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: saveTimer.running ? "Auto-saving..." : "Auto-saved"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: saveTimer.running ? Theme.primary : Theme.surfaceTextMedium
|
|
||||||
opacity: SessionData.notepadContent.length > 0 ? 1 : 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: saveTimer
|
|
||||||
|
|
||||||
interval: 1000
|
|
||||||
repeat: false
|
|
||||||
onTriggered: SessionData.saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: saveBrowser
|
|
||||||
|
|
||||||
browserTitle: "Save Notepad File"
|
|
||||||
browserIcon: "save"
|
|
||||||
browserType: "notepad_save"
|
|
||||||
fileExtensions: ["*.txt", "*.*"]
|
|
||||||
allowStacking: true
|
|
||||||
saveMode: true
|
|
||||||
defaultFileName: "note.txt"
|
|
||||||
|
|
||||||
onFileSelected: (path) => {
|
|
||||||
fileDialogOpen = false;
|
|
||||||
selectedFilePath = path;
|
|
||||||
const content = textArea.text;
|
|
||||||
if (content.length > 0) {
|
|
||||||
writeFileProcess.command = ["sh", "-c", `echo '${content.replace(/'/g, "'\\''")}' > '${path}'`];
|
|
||||||
writeFileProcess.running = true;
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
// Restore modal focus
|
|
||||||
root.allowFocusOverride = false;
|
|
||||||
root.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return root.notepadModalVisible && !fileDialogOpen;
|
|
||||||
});
|
|
||||||
// Restore focus to TextArea after dialog closes
|
|
||||||
Qt.callLater(() => {
|
|
||||||
textArea.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDialogClosed: {
|
|
||||||
fileDialogOpen = false;
|
|
||||||
// Restore modal focus
|
|
||||||
root.allowFocusOverride = false;
|
|
||||||
root.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return root.notepadModalVisible && !fileDialogOpen;
|
|
||||||
});
|
|
||||||
// Restore focus to TextArea after dialog closes
|
|
||||||
Qt.callLater(() => {
|
|
||||||
textArea.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
property string selectedFilePath: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: loadBrowser
|
|
||||||
|
|
||||||
browserTitle: "Load Notepad File"
|
|
||||||
browserIcon: "folder_open"
|
|
||||||
browserType: "notepad_load"
|
|
||||||
fileExtensions: ["*.txt", "*.*"]
|
|
||||||
allowStacking: true
|
|
||||||
|
|
||||||
onFileSelected: (path) => {
|
|
||||||
fileDialogOpen = false;
|
|
||||||
// Clean the file path - remove file:// prefix if present
|
|
||||||
var cleanPath = path.toString().replace(/^file:\/\//, '');
|
|
||||||
// Extract filename from path
|
|
||||||
var fileName = cleanPath.split('/').pop();
|
|
||||||
currentFileName = fileName;
|
|
||||||
console.log("Notepad: Loading file from path:", cleanPath);
|
|
||||||
readFileProcess.command = ["cat", cleanPath];
|
|
||||||
readFileProcess.running = true;
|
|
||||||
close();
|
|
||||||
// Restore modal focus
|
|
||||||
root.allowFocusOverride = false;
|
|
||||||
root.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return root.notepadModalVisible && !fileDialogOpen;
|
|
||||||
});
|
|
||||||
// Restore focus to TextArea after dialog closes
|
|
||||||
Qt.callLater(() => {
|
|
||||||
textArea.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDialogClosed: {
|
|
||||||
fileDialogOpen = false;
|
|
||||||
// Restore modal focus
|
|
||||||
root.allowFocusOverride = false;
|
|
||||||
root.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return root.notepadModalVisible && !fileDialogOpen;
|
|
||||||
});
|
|
||||||
// Restore focus to TextArea after dialog closes
|
|
||||||
Qt.callLater(() => {
|
|
||||||
textArea.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: writeFileProcess
|
|
||||||
|
|
||||||
command: []
|
|
||||||
running: false
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (exitCode === 0)
|
|
||||||
console.log("Notepad: File saved successfully");
|
|
||||||
else
|
|
||||||
console.warn("Notepad: Failed to save file, exit code:", exitCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: readFileProcess
|
|
||||||
|
|
||||||
command: []
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
console.log("Notepad: File content loaded, length:", text.length);
|
|
||||||
textArea.text = text;
|
|
||||||
SessionData.notepadContent = text;
|
|
||||||
SessionData.saveSettings();
|
|
||||||
console.log("Notepad: File loaded and saved to session");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
console.log("Notepad: File read process exited with code:", exitCode);
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("Notepad: Failed to load file, exit code:", exitCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int optionCount: 4
|
|
||||||
|
|
||||||
signal powerActionRequested(string action, string title, string message)
|
|
||||||
|
|
||||||
function selectOption() {
|
|
||||||
close()
|
|
||||||
switch (selectedIndex) {
|
|
||||||
case 0:
|
|
||||||
root.powerActionRequested("logout", "Log Out", "Are you sure you want to log out?")
|
|
||||||
break
|
|
||||||
case 1:
|
|
||||||
root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend the system?")
|
|
||||||
break
|
|
||||||
case 2:
|
|
||||||
root.powerActionRequested("reboot", "Reboot", "Are you sure you want to reboot the system?")
|
|
||||||
break
|
|
||||||
case 3:
|
|
||||||
root.powerActionRequested("poweroff", "Power Off", "Are you sure you want to power off the system?")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
width: 320
|
|
||||||
height: 300
|
|
||||||
enableShadow: true
|
|
||||||
onBackgroundClicked: {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
onOpened: {
|
|
||||||
selectedIndex = 0
|
|
||||||
modalFocusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: function(event) {
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Up:
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Down:
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
selectOption()
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Power Options"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
|
||||||
onClicked: {
|
|
||||||
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: "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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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: "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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 2)
|
|
||||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
|
||||||
else if (rebootArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08);
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 2 ? Theme.warning : "transparent"
|
|
||||||
border.width: selectedIndex === 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: "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 = 2
|
|
||||||
selectOption()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 3)
|
|
||||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
|
||||||
else if (powerOffArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 3 ? Theme.error : "transparent"
|
|
||||||
border.width: selectedIndex === 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: "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 = 3
|
|
||||||
selectOption()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
height: Theme.spacingS
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "↑↓ Navigate • Tab Cycle • Enter Select • Esc Close"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: processListModal
|
|
||||||
|
|
||||||
property int currentTab: 0
|
|
||||||
property var tabNames: ["Processes", "Performance", "System"]
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
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: 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: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
processListModal.hide()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_1) {
|
|
||||||
currentTab = 0
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_2) {
|
|
||||||
currentTab = 1
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_3) {
|
|
||||||
currentTab = 2
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: "System Monitor Unavailable"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.error
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "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: "System Monitor"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge + 4
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: processListModal.hide()
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 52
|
|
||||||
color: Theme.surfaceSelected
|
|
||||||
radius: Theme.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: {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
return "list_alt"
|
|
||||||
case 1:
|
|
||||||
return "analytics"
|
|
||||||
case 2:
|
|
||||||
return "settings"
|
|
||||||
default:
|
|
||||||
return "tab"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: currentTab
|
|
||||||
=== index ? Theme.primary : Theme.surfaceText
|
|
||||||
opacity: currentTab === index ? 1 : 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: currentTab
|
|
||||||
=== index ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.verticalCenterOffset: -1
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: tabMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
currentTab = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
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,606 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Settings
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: settingsModal
|
|
||||||
|
|
||||||
property Component settingsContent
|
|
||||||
|
|
||||||
signal closingModal
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible)
|
|
||||||
hide()
|
|
||||||
else
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
objectName: "settingsModal"
|
|
||||||
width: 800
|
|
||||||
height: 750
|
|
||||||
visible: false
|
|
||||||
onBackgroundClicked: hide()
|
|
||||||
content: settingsContent
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open() {
|
|
||||||
settingsModal.show()
|
|
||||||
return "SETTINGS_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
settingsModal.hide()
|
|
||||||
return "SETTINGS_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsContent: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
anchors.bottomMargin: Theme.spacingL
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 35
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "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
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: settingsModal.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 35
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: sidebarContainer
|
|
||||||
|
|
||||||
property int currentIndex: 0
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 110
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: profileImageContainer
|
|
||||||
width: 80
|
|
||||||
height: 80
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
property bool hasImage: profileImageSource.status
|
|
||||||
=== Image.Ready
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 1
|
|
||||||
visible: parent.hasImage
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: profileImageSource
|
|
||||||
source: {
|
|
||||||
if (PortalService.profileImage === "")
|
|
||||||
return ""
|
|
||||||
if (PortalService.profileImage.startsWith(
|
|
||||||
"/"))
|
|
||||||
return "file://" + PortalService.profileImage
|
|
||||||
return PortalService.profileImage
|
|
||||||
}
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
mipmap: true
|
|
||||||
cache: true
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
source: profileImageSource
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: profileCircularMask
|
|
||||||
visible: profileImageContainer.hasImage
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: profileCircularMask
|
|
||||||
width: 70
|
|
||||||
height: 70
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: Theme.primary
|
|
||||||
visible: !parent.hasImage
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "person"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primaryText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.7)
|
|
||||||
visible: profileMouseArea.containsMouse
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: Qt.rgba(255, 255,
|
|
||||||
255, 0.9)
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "edit"
|
|
||||||
size: 16
|
|
||||||
color: "black"
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
settingsModal.allowFocusOverride = true
|
|
||||||
settingsModal.shouldHaveFocus = false
|
|
||||||
profileBrowser.open(
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: Qt.rgba(255, 255,
|
|
||||||
255, 0.9)
|
|
||||||
visible: profileImageContainer.hasImage
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "close"
|
|
||||||
size: 16
|
|
||||||
color: "black"
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
PortalService.setProfileImage(
|
|
||||||
"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: profileMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
propagateComposedEvents: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: 120
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: UserInfoService.fullName
|
|
||||||
|| "User"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.distribution
|
|
||||||
|| "Linux"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: [{
|
|
||||||
"text": "Personalization",
|
|
||||||
"icon": "person"
|
|
||||||
}, {
|
|
||||||
"text": "Time & Date",
|
|
||||||
"icon": "schedule"
|
|
||||||
}, {
|
|
||||||
"text": "Weather",
|
|
||||||
"icon": "cloud"
|
|
||||||
}, {
|
|
||||||
"text": "Top Bar",
|
|
||||||
"icon": "toolbar"
|
|
||||||
}, {
|
|
||||||
"text": "Widgets",
|
|
||||||
"icon": "widgets"
|
|
||||||
}, {
|
|
||||||
"text": "Dock",
|
|
||||||
"icon": "dock_to_bottom"
|
|
||||||
}, {
|
|
||||||
"text": "Displays",
|
|
||||||
"icon": "monitor"
|
|
||||||
}, {
|
|
||||||
"text": "Recent Apps",
|
|
||||||
"icon": "history"
|
|
||||||
}, {
|
|
||||||
"text": "Theme & Colors",
|
|
||||||
"icon": "palette"
|
|
||||||
}, {
|
|
||||||
"text": "About",
|
|
||||||
"icon": "info"
|
|
||||||
}]
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool isActive: sidebarContainer.currentIndex === index
|
|
||||||
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 44
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: isActive ? Theme.surfaceContainerHigh : 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.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.text || ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: parent.parent.isActive ? Theme.primary : 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - sidebarContainer.width
|
|
||||||
height: parent.height
|
|
||||||
|
|
||||||
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: sidebarContainer.currentIndex === 0
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
PersonalizationTab {
|
|
||||||
parentModal: settingsModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: timeLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 1
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: TimeTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: weatherLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 2
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: WeatherTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: topBarLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 3
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: TopBarTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: widgetsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 4
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: WidgetTweaksTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: dockLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 5
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
DockTab {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: displaysLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 6
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: DisplaysTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: recentAppsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 7
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: RecentAppsTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: themeColorsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 8
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: ThemeColorsTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: aboutLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 9
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: AboutTab {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: profileBrowser
|
|
||||||
|
|
||||||
allowStacking: true
|
|
||||||
browserTitle: "Select Profile Image"
|
|
||||||
browserIcon: "person"
|
|
||||||
browserType: "profile"
|
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
onFileSelected: path => {
|
|
||||||
PortalService.setProfileImage(path)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
onDialogClosed: {
|
|
||||||
if (settingsModal) {
|
|
||||||
settingsModal.allowFocusOverride = false
|
|
||||||
settingsModal.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return settingsModal.shouldBeVisible
|
|
||||||
})
|
|
||||||
}
|
|
||||||
allowStacking = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: wallpaperBrowser
|
|
||||||
|
|
||||||
allowStacking: true
|
|
||||||
browserTitle: "Select Wallpaper"
|
|
||||||
browserIcon: "wallpaper"
|
|
||||||
browserType: "wallpaper"
|
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
onFileSelected: path => {
|
|
||||||
SessionData.setWallpaper(path)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
onDialogClosed: {
|
|
||||||
allowStacking = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,914 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.AppDrawer
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: spotlightModal
|
|
||||||
|
|
||||||
property bool spotlightOpen: false
|
|
||||||
property Component spotlightContent
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
spotlightOpen = true
|
|
||||||
open()
|
|
||||||
if (contentLoader.item && contentLoader.item.appLauncher)
|
|
||||||
contentLoader.item.appLauncher.searchQuery = ""
|
|
||||||
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item && contentLoader.item.searchField)
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
spotlightOpen = false
|
|
||||||
close()
|
|
||||||
if (contentLoader.item && contentLoader.item.appLauncher) {
|
|
||||||
contentLoader.item.appLauncher.searchQuery = ""
|
|
||||||
contentLoader.item.appLauncher.selectedIndex = 0
|
|
||||||
contentLoader.item.appLauncher.setCategory("All")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (spotlightOpen)
|
|
||||||
hide()
|
|
||||||
else
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: spotlightOpen
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: ModalManager
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== spotlightModal && !allowStacking
|
|
||||||
&& spotlightOpen) {
|
|
||||||
spotlightOpen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width: 550
|
|
||||||
height: 600
|
|
||||||
backgroundColor: Theme.popupBackground()
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
borderColor: Theme.outlineMedium
|
|
||||||
borderWidth: 1
|
|
||||||
enableShadow: true
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && !spotlightOpen)
|
|
||||||
show()
|
|
||||||
|
|
||||||
if (visible && contentLoader.item)
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item.searchField)
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onBackgroundClicked: {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
|
|
||||||
}
|
|
||||||
content: spotlightContent
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open() {
|
|
||||||
spotlightModal.show()
|
|
||||||
return "SPOTLIGHT_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
spotlightModal.hide()
|
|
||||||
return "SPOTLIGHT_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
spotlightModal.toggle()
|
|
||||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "spotlight"
|
|
||||||
}
|
|
||||||
|
|
||||||
spotlightContent: Component {
|
|
||||||
Item {
|
|
||||||
id: spotlightKeyHandler
|
|
||||||
|
|
||||||
property alias appLauncher: appLauncher
|
|
||||||
property alias searchField: searchField
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
appLauncher.selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
appLauncher.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Right
|
|
||||||
&& appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Left
|
|
||||||
&& appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Return
|
|
||||||
|| event.key === Qt.Key_Enter) {
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (!searchField.activeFocus && event.text
|
|
||||||
&& event.text.length > 0 && event.text.match(
|
|
||||||
/[a-zA-Z0-9\\s]/)) {
|
|
||||||
searchField.forceActiveFocus()
|
|
||||||
searchField.insertText(event.text)
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppLauncher {
|
|
||||||
id: appLauncher
|
|
||||||
|
|
||||||
viewMode: SettingsData.spotlightModalViewMode
|
|
||||||
gridColumns: 4
|
|
||||||
onAppLaunched: hide()
|
|
||||||
onViewModeSelected: function (mode) {
|
|
||||||
SettingsData.setSpotlightModalViewMode(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: categorySelector.height + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceVariantAlpha
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
visible: appLauncher.categories.length > 1
|
|
||||||
|| appLauncher.model.count > 0
|
|
||||||
|
|
||||||
CategorySelector {
|
|
||||||
id: categorySelector
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
categories: appLauncher.categories
|
|
||||||
selectedCategory: appLauncher.selectedCategory
|
|
||||||
compact: false
|
|
||||||
onCategorySelected: category => {
|
|
||||||
return appLauncher.setCategory(
|
|
||||||
category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
|
|
||||||
width: parent.width - 80
|
|
||||||
- Theme.spacingM // Leave space for view toggle buttons
|
|
||||||
height: 56
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
backgroundColor: Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha(
|
|
||||||
) * 0.7)
|
|
||||||
normalBorderColor: Theme.outlineMedium
|
|
||||||
focusedBorderColor: Theme.primary
|
|
||||||
leftIconName: "search"
|
|
||||||
leftIconSize: Theme.iconSize
|
|
||||||
leftIconColor: Theme.surfaceVariantText
|
|
||||||
leftIconFocusedColor: Theme.primary
|
|
||||||
showClearButton: true
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
enabled: spotlightOpen
|
|
||||||
placeholderText: ""
|
|
||||||
ignoreLeftRightKeys: true
|
|
||||||
keyForwardTargets: [spotlightKeyHandler]
|
|
||||||
text: appLauncher.searchQuery
|
|
||||||
onTextEdited: {
|
|
||||||
appLauncher.searchQuery = text
|
|
||||||
}
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
} else if ((event.key === Qt.Key_Return
|
|
||||||
|| event.key === Qt.Key_Enter)
|
|
||||||
&& text.length > 0) {
|
|
||||||
if (appLauncher.keyboardNavigationActive
|
|
||||||
&& appLauncher.model.count > 0)
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
else if (appLauncher.model.count > 0)
|
|
||||||
appLauncher.launchApp(
|
|
||||||
appLauncher.model.get(0))
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
border.color: appLauncher.viewMode
|
|
||||||
=== "list" ? Theme.primarySelected : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "view_list"
|
|
||||||
size: 18
|
|
||||||
color: appLauncher.viewMode
|
|
||||||
=== "list" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: listViewArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
appLauncher.setViewMode("list")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
border.color: appLauncher.viewMode
|
|
||||||
=== "grid" ? Theme.primarySelected : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "grid_view"
|
|
||||||
size: 18
|
|
||||||
color: appLauncher.viewMode
|
|
||||||
=== "grid" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gridViewArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
appLauncher.setViewMode("grid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: resultsContainer
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - y
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
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.keyboardNavigationActive
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
var itemY = index * (itemHeight + itemSpacing)
|
|
||||||
var 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.viewMode === "list"
|
|
||||||
model: appLauncher.model
|
|
||||||
currentIndex: appLauncher.selectedIndex
|
|
||||||
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: function (index, modelData) {
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOn
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: ListView.view.width
|
|
||||||
height: resultsList.itemHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
|
||||||
border.width: ListView.isCurrentItem ? 2 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: resultsList.iconSize
|
|
||||||
height: resultsList.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: listIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Quickshell.iconPath(model.icon, true)
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !listIconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name
|
|
||||||
&& model.name.length
|
|
||||||
> 0) ? model.name.charAt(
|
|
||||||
0).toUpperCase(
|
|
||||||
) : "A"
|
|
||||||
font.pixelSize: resultsList.iconSize * 0.4
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - resultsList.iconSize - Theme.spacingL
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.comment || "Application"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
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) {
|
|
||||||
var modalPos = mapToItem(
|
|
||||||
spotlightKeyHandler,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
resultsList.itemRightClicked(
|
|
||||||
index, model,
|
|
||||||
modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: resultsGrid
|
|
||||||
|
|
||||||
property int currentIndex: appLauncher.selectedIndex
|
|
||||||
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.keyboardNavigationActive
|
|
||||||
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
|
|
||||||
|
|
||||||
var itemY = Math.floor(
|
|
||||||
index / actualColumns) * cellHeight
|
|
||||||
var 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.viewMode === "grid"
|
|
||||||
model: appLauncher.model
|
|
||||||
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: function (index, modelData) {
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
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 : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: resultsGrid.currentIndex
|
|
||||||
=== index ? Theme.primarySelected : Theme.outlineMedium
|
|
||||||
border.width: resultsGrid.currentIndex === index ? 2 : 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int iconSize: Math.min(
|
|
||||||
resultsGrid.maxIconSize,
|
|
||||||
Math.max(
|
|
||||||
resultsGrid.minIconSize,
|
|
||||||
resultsGrid.cellWidth
|
|
||||||
* resultsGrid.iconSizeRatio))
|
|
||||||
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: gridIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Quickshell.iconPath(model.icon, true)
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !gridIconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name
|
|
||||||
&& model.name.length
|
|
||||||
> 0) ? model.name.charAt(
|
|
||||||
0).toUpperCase(
|
|
||||||
) : "A"
|
|
||||||
font.pixelSize: Math.min(
|
|
||||||
28,
|
|
||||||
parent.width * 0.5)
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
width: resultsGrid.cellWidth - 12
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
maximumLineCount: 2
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gridMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
z: 10
|
|
||||||
onEntered: {
|
|
||||||
if (resultsGrid.hoverUpdatesSelection
|
|
||||||
&& !resultsGrid.keyboardNavigationActive)
|
|
||||||
resultsGrid.currentIndex = index
|
|
||||||
}
|
|
||||||
onPositionChanged: {
|
|
||||||
resultsGrid.keyboardNavigationReset()
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
resultsGrid.itemClicked(
|
|
||||||
index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
var modalPos = mapToItem(
|
|
||||||
spotlightKeyHandler,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
resultsGrid.itemRightClicked(
|
|
||||||
index, model,
|
|
||||||
modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
property var currentApp: null
|
|
||||||
property bool menuVisible: false
|
|
||||||
|
|
||||||
function show(x, y, app) {
|
|
||||||
currentApp = app
|
|
||||||
|
|
||||||
const menuWidth = 180
|
|
||||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
let finalX = x + 8
|
|
||||||
let finalY = y + 8
|
|
||||||
|
|
||||||
if (finalX + menuWidth > spotlightKeyHandler.width) {
|
|
||||||
finalX = x - menuWidth - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalY + menuHeight > spotlightKeyHandler.height) {
|
|
||||||
finalY = y - menuHeight - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
finalX = Math.max(
|
|
||||||
8, Math.min(
|
|
||||||
finalX,
|
|
||||||
spotlightKeyHandler.width - menuWidth - 8))
|
|
||||||
finalY = Math.max(
|
|
||||||
8, Math.min(
|
|
||||||
finalY,
|
|
||||||
spotlightKeyHandler.height - menuHeight - 8))
|
|
||||||
|
|
||||||
contextMenu.x = finalX
|
|
||||||
contextMenu.y = finalY
|
|
||||||
contextMenu.visible = true
|
|
||||||
contextMenu.menuVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
contextMenu.menuVisible = false
|
|
||||||
Qt.callLater(() => {
|
|
||||||
contextMenu.visible = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 180
|
|
||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
opacity: menuVisible ? 1 : 0
|
|
||||||
scale: menuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: pinMouseArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "push_pin"
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
return SessionData.isPinnedApp(
|
|
||||||
appId) ? "keep_off" : "push_pin"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "Pin to Dock"
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
return SessionData.isPinnedApp(
|
|
||||||
appId) ? "Unpin from Dock" : "Pin to Dock"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: pinMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
if (SessionData.isPinnedApp(appId))
|
|
||||||
SessionData.removePinnedApp(appId)
|
|
||||||
else
|
|
||||||
SessionData.addPinnedApp(appId)
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: launchMouseArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "launch"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Launch"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: launchMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (contextMenu.currentApp)
|
|
||||||
appLauncher.launchApp(
|
|
||||||
contextMenu.currentApp)
|
|
||||||
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: contextMenu.visible
|
|
||||||
z: 999
|
|
||||||
onClicked: {
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: contextMenu.x
|
|
||||||
y: contextMenu.y
|
|
||||||
width: contextMenu.width
|
|
||||||
height: contextMenu.height
|
|
||||||
onClicked: {
|
|
||||||
|
|
||||||
// Prevent closing when clicking on the menu itself
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string wifiPasswordSSID: ""
|
|
||||||
property string wifiPasswordInput: ""
|
|
||||||
|
|
||||||
function show(ssid) {
|
|
||||||
wifiPasswordSSID = ssid
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
open()
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item && contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
width: 420
|
|
||||||
height: 230
|
|
||||||
onShouldBeVisibleChanged: {
|
|
||||||
if (!shouldBeVisible)
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
|
||||||
onOpened: {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item && contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onBackgroundClicked: {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onPasswordDialogShouldReopenChanged() {
|
|
||||||
if (NetworkService.passwordDialogShouldReopen
|
|
||||||
&& NetworkService.connectingSSID !== "") {
|
|
||||||
wifiPasswordSSID = NetworkService.connectingSSID
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
open()
|
|
||||||
NetworkService.passwordDialogShouldReopen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: NetworkService
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
FocusScope {
|
|
||||||
id: wifiContent
|
|
||||||
property alias passwordInput: passwordInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onEscapePressed: function (event) {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 40
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Connect to Wi-Fi"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Enter password for \"" + wifiPasswordSSID + "\""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: passwordInput.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: passwordInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiPasswordInput
|
|
||||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
|
||||||
placeholderText: ""
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
focus: true
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: {
|
|
||||||
wifiPasswordInput = text
|
|
||||||
}
|
|
||||||
onAccepted: {
|
|
||||||
NetworkService.connectToWifiWithPassword(
|
|
||||||
wifiPasswordSSID, passwordInput.text)
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
passwordInput.text = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: focusDelayTimer
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: showPasswordCheckbox
|
|
||||||
|
|
||||||
property bool checked: false
|
|
||||||
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
radius: 4
|
|
||||||
color: checked ? Theme.primary : "transparent"
|
|
||||||
border.color: checked ? Theme.primary : Theme.outlineButton
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "check"
|
|
||||||
size: 12
|
|
||||||
color: Theme.background
|
|
||||||
visible: parent.checked
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show password"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(
|
|
||||||
70,
|
|
||||||
cancelText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: cancelText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Cancel"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cancelArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(
|
|
||||||
80,
|
|
||||||
connectText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: connectArea.containsMouse ? Qt.darker(
|
|
||||||
Theme.primary,
|
|
||||||
1.1) : Theme.primary
|
|
||||||
enabled: passwordInput.text.length > 0
|
|
||||||
opacity: enabled ? 1 : 0.5
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: connectText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Connect"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.background
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: connectArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: parent.enabled
|
|
||||||
onClicked: {
|
|
||||||
NetworkService.connectToWifiWithPassword(
|
|
||||||
wifiPasswordSSID,
|
|
||||||
passwordInput.text)
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
passwordInput.text = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,945 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.AppDrawer
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankPopout {
|
|
||||||
id: appDrawerPopout
|
|
||||||
|
|
||||||
property string triggerSection: "left"
|
|
||||||
property var triggerScreen: null
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
open()
|
|
||||||
appLauncher.searchQuery = ""
|
|
||||||
appLauncher.selectedIndex = 0
|
|
||||||
appLauncher.setCategory("All")
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
triggerX = x
|
|
||||||
triggerY = y
|
|
||||||
triggerWidth = width
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
}
|
|
||||||
|
|
||||||
popupWidth: 520
|
|
||||||
popupHeight: 600
|
|
||||||
triggerX: Theme.spacingL
|
|
||||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
|
||||||
triggerWidth: 40
|
|
||||||
positioning: "center"
|
|
||||||
WlrLayershell.namespace: "quickshell-launcher"
|
|
||||||
screen: triggerScreen
|
|
||||||
|
|
||||||
onOpened: {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item && contentLoader.item.searchField) {
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
AppLauncher {
|
|
||||||
id: appLauncher
|
|
||||||
|
|
||||||
viewMode: SettingsData.appLauncherViewMode
|
|
||||||
gridColumns: 4
|
|
||||||
onAppLaunched: appDrawerPopout.hide()
|
|
||||||
onViewModeSelected: function (mode) {
|
|
||||||
SettingsData.setAppLauncherViewMode(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Rectangle {
|
|
||||||
id: launcherPanel
|
|
||||||
|
|
||||||
property alias searchField: searchField
|
|
||||||
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
antialiasing: true
|
|
||||||
smooth: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -3
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 3
|
|
||||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
|
||||||
border.width: 1
|
|
||||||
z: -3
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -2
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 2
|
|
||||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: -2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.12)
|
|
||||||
border.width: 1
|
|
||||||
radius: parent.radius
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: keyHandler
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
appDrawerPopout.close()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
appLauncher.selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
appLauncher.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Right
|
|
||||||
&& appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Left
|
|
||||||
&& appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Return
|
|
||||||
|| event.key === Qt.Key_Enter) {
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (!searchField.activeFocus && event.text
|
|
||||||
&& event.text.length > 0 && event.text.match(
|
|
||||||
/[a-zA-Z0-9\\s]/)) {
|
|
||||||
searchField.forceActiveFocus()
|
|
||||||
searchField.insertText(event.text)
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.spacingL * 2
|
|
||||||
height: parent.height - Theme.spacingL * 2
|
|
||||||
x: Theme.spacingL
|
|
||||||
y: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "Applications"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge + 4
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 200
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: appLauncher.model.count + " apps"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 52
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
backgroundColor: Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha(
|
|
||||||
) * 0.7)
|
|
||||||
normalBorderColor: Qt.rgba(Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
focusedBorderColor: Theme.primary
|
|
||||||
leftIconName: "search"
|
|
||||||
leftIconSize: Theme.iconSize
|
|
||||||
leftIconColor: Theme.surfaceVariantText
|
|
||||||
leftIconFocusedColor: Theme.primary
|
|
||||||
showClearButton: true
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
enabled: appDrawerPopout.shouldBeVisible
|
|
||||||
ignoreLeftRightKeys: true
|
|
||||||
keyForwardTargets: [keyHandler]
|
|
||||||
onTextEdited: {
|
|
||||||
appLauncher.searchQuery = text
|
|
||||||
}
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
appDrawerPopout.close()
|
|
||||||
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) {
|
|
||||||
var firstApp = appLauncher.model.get(0)
|
|
||||||
appLauncher.launchApp(firstApp)
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down || event.key
|
|
||||||
=== Qt.Key_Up || event.key === Qt.Key_Left || event.key
|
|
||||||
=== Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (!appDrawerPopout.shouldBeVisible)
|
|
||||||
searchField.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: appDrawerPopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: searchField.text.length === 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 200
|
|
||||||
height: 36
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
anchors.fill: parent
|
|
||||||
text: ""
|
|
||||||
currentValue: appLauncher.selectedCategory
|
|
||||||
options: appLauncher.categories
|
|
||||||
optionIcons: appLauncher.categoryIcons
|
|
||||||
onValueChanged: function (value) {
|
|
||||||
appLauncher.setCategory(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 300
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 36
|
|
||||||
circular: false
|
|
||||||
iconName: "view_list"
|
|
||||||
iconSize: 20
|
|
||||||
iconColor: appLauncher.viewMode
|
|
||||||
=== "list" ? Theme.primary : Theme.surfaceText
|
|
||||||
hoverColor: appLauncher.viewMode
|
|
||||||
=== "list" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
backgroundColor: appLauncher.viewMode
|
|
||||||
=== "list" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
onClicked: {
|
|
||||||
appLauncher.setViewMode("list")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 36
|
|
||||||
circular: false
|
|
||||||
iconName: "grid_view"
|
|
||||||
iconSize: 20
|
|
||||||
iconColor: appLauncher.viewMode
|
|
||||||
=== "grid" ? Theme.primary : Theme.surfaceText
|
|
||||||
hoverColor: appLauncher.viewMode
|
|
||||||
=== "grid" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
backgroundColor: appLauncher.viewMode
|
|
||||||
=== "grid" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
onClicked: {
|
|
||||||
appLauncher.setViewMode("grid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: {
|
|
||||||
let usedHeight = 40 + Theme.spacingL
|
|
||||||
usedHeight += 52 + Theme.spacingL
|
|
||||||
usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0)
|
|
||||||
return parent.height - usedHeight
|
|
||||||
}
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.1)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.05)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: appList
|
|
||||||
|
|
||||||
property int itemHeight: 72
|
|
||||||
property int iconSize: 56
|
|
||||||
property bool showDescription: true
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
var itemY = index * (itemHeight + itemSpacing)
|
|
||||||
var 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.viewMode === "list"
|
|
||||||
model: appLauncher.model
|
|
||||||
currentIndex: appLauncher.selectedIndex
|
|
||||||
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: function (index, modelData) {
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOn
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: ListView.view.width
|
|
||||||
height: appList.itemHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
|
||||||
border.width: ListView.isCurrentItem ? 2 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: appList.iconSize
|
|
||||||
height: appList.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: listIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Quickshell.iconPath(model.icon, true)
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !listIconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name
|
|
||||||
&& model.name.length
|
|
||||||
> 0) ? model.name.charAt(
|
|
||||||
0).toUpperCase(
|
|
||||||
) : "A"
|
|
||||||
font.pixelSize: appList.iconSize * 0.4
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - appList.iconSize - Theme.spacingL
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.comment || "Application"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
visible: appList.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 (appList.hoverUpdatesSelection
|
|
||||||
&& !appList.keyboardNavigationActive)
|
|
||||||
appList.currentIndex = index
|
|
||||||
}
|
|
||||||
onPositionChanged: {
|
|
||||||
appList.keyboardNavigationReset()
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
appList.itemClicked(
|
|
||||||
index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
var panelPos = mapToItem(
|
|
||||||
contextMenu.parent,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
appList.itemRightClicked(
|
|
||||||
index, model,
|
|
||||||
panelPos.x,
|
|
||||||
panelPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: appGrid
|
|
||||||
|
|
||||||
property int currentIndex: appLauncher.selectedIndex
|
|
||||||
property int columns: 4
|
|
||||||
property bool adaptiveColumns: false
|
|
||||||
property int minCellWidth: 120
|
|
||||||
property int maxCellWidth: 160
|
|
||||||
property int cellPadding: 8
|
|
||||||
property real iconSizeRatio: 0.6
|
|
||||||
property int maxIconSize: 56
|
|
||||||
property int minIconSize: 32
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
|
||||||
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
|
|
||||||
|
|
||||||
var itemY = Math.floor(
|
|
||||||
index / actualColumns) * cellHeight
|
|
||||||
var 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.viewMode === "grid"
|
|
||||||
model: appLauncher.model
|
|
||||||
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: function (index, modelData) {
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: appGrid.cellWidth - appGrid.cellPadding
|
|
||||||
height: appGrid.cellHeight - appGrid.cellPadding
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: appGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
|
|
||||||
border.width: appGrid.currentIndex === index ? 2 : 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int iconSize: Math.min(
|
|
||||||
appGrid.maxIconSize,
|
|
||||||
Math.max(
|
|
||||||
appGrid.minIconSize,
|
|
||||||
appGrid.cellWidth
|
|
||||||
* appGrid.iconSizeRatio))
|
|
||||||
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: gridIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: Quickshell.iconPath(model.icon, true)
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !gridIconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name
|
|
||||||
&& model.name.length
|
|
||||||
> 0) ? model.name.charAt(
|
|
||||||
0).toUpperCase(
|
|
||||||
) : "A"
|
|
||||||
font.pixelSize: Math.min(
|
|
||||||
28,
|
|
||||||
parent.width * 0.5)
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
width: appGrid.cellWidth - 12
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
maximumLineCount: 2
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gridMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
z: 10
|
|
||||||
onEntered: {
|
|
||||||
if (appGrid.hoverUpdatesSelection
|
|
||||||
&& !appGrid.keyboardNavigationActive)
|
|
||||||
appGrid.currentIndex = index
|
|
||||||
}
|
|
||||||
onPositionChanged: {
|
|
||||||
appGrid.keyboardNavigationReset()
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
appGrid.itemClicked(
|
|
||||||
index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
var panelPos = mapToItem(
|
|
||||||
contextMenu.parent,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
appGrid.itemRightClicked(
|
|
||||||
index, model,
|
|
||||||
panelPos.x,
|
|
||||||
panelPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
property var currentApp: null
|
|
||||||
property bool menuVisible: false
|
|
||||||
|
|
||||||
function show(x, y, app) {
|
|
||||||
currentApp = app
|
|
||||||
|
|
||||||
const menuWidth = 180
|
|
||||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
|
|
||||||
let finalX = x + 8
|
|
||||||
let finalY = y + 8
|
|
||||||
|
|
||||||
if (finalX + menuWidth > appDrawerPopout.popupWidth) {
|
|
||||||
finalX = x - menuWidth - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalY + menuHeight > appDrawerPopout.popupHeight) {
|
|
||||||
finalY = y - menuHeight - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
finalX = Math.max(
|
|
||||||
8, Math.min(finalX,
|
|
||||||
appDrawerPopout.popupWidth - menuWidth - 8))
|
|
||||||
finalY = Math.max(8, Math.min(
|
|
||||||
finalY,
|
|
||||||
appDrawerPopout.popupHeight - menuHeight - 8))
|
|
||||||
|
|
||||||
contextMenu.x = finalX
|
|
||||||
contextMenu.y = finalY
|
|
||||||
contextMenu.visible = true
|
|
||||||
contextMenu.menuVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
contextMenu.menuVisible = false
|
|
||||||
Qt.callLater(() => {
|
|
||||||
contextMenu.visible = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 180
|
|
||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
opacity: menuVisible ? 1 : 0
|
|
||||||
scale: menuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: pinMouseArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "push_pin"
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
return SessionData.isPinnedApp(
|
|
||||||
appId) ? "keep_off" : "push_pin"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "Pin to Dock"
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
return SessionData.isPinnedApp(
|
|
||||||
appId) ? "Unpin from Dock" : "Pin to Dock"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: pinMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
if (SessionData.isPinnedApp(appId))
|
|
||||||
SessionData.removePinnedApp(appId)
|
|
||||||
else
|
|
||||||
SessionData.addPinnedApp(appId)
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: launchMouseArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "launch"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Launch"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: launchMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (contextMenu.currentApp)
|
|
||||||
appLauncher.launchApp(contextMenu.currentApp)
|
|
||||||
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: contextMenu.visible
|
|
||||||
z: 999
|
|
||||||
onClicked: {
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: contextMenu.x
|
|
||||||
y: contextMenu.y
|
|
||||||
width: contextMenu.width
|
|
||||||
height: contextMenu.height
|
|
||||||
onClicked: {
|
|
||||||
|
|
||||||
// Prevent closing when clicking on the menu itself
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string searchQuery: ""
|
|
||||||
property string selectedCategory: "All"
|
|
||||||
property string viewMode: "list" // "list" or "grid"
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int maxResults: 50
|
|
||||||
property int gridColumns: 4
|
|
||||||
property bool debounceSearch: true
|
|
||||||
property int debounceInterval: 50
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property var categories: {
|
|
||||||
var allCategories = AppSearchService.getAllCategories().filter(cat => {
|
|
||||||
return cat !== "Education"
|
|
||||||
&& cat !== "Science"
|
|
||||||
})
|
|
||||||
var result = ["All"]
|
|
||||||
return result.concat(allCategories.filter(cat => {
|
|
||||||
return cat !== "All"
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
property var categoryIcons: categories.map(category => {
|
|
||||||
return AppSearchService.getCategoryIcon(
|
|
||||||
category)
|
|
||||||
})
|
|
||||||
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
|
||||||
property alias model: filteredModel
|
|
||||||
property var _watchApplications: AppSearchService.applications
|
|
||||||
|
|
||||||
signal appLaunched(var app)
|
|
||||||
signal categorySelected(string category)
|
|
||||||
signal viewModeSelected(string mode)
|
|
||||||
|
|
||||||
function updateFilteredModel() {
|
|
||||||
filteredModel.clear()
|
|
||||||
selectedIndex = 0
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
var apps = []
|
|
||||||
if (searchQuery.length === 0) {
|
|
||||||
if (selectedCategory === "All") {
|
|
||||||
apps = AppSearchService.getAppsInCategory(
|
|
||||||
"All") // HACK: Use function call instead of property
|
|
||||||
} else {
|
|
||||||
var categoryApps = AppSearchService.getAppsInCategory(
|
|
||||||
selectedCategory)
|
|
||||||
apps = categoryApps.slice(0, maxResults)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (selectedCategory === "All") {
|
|
||||||
apps = AppSearchService.searchApplications(searchQuery)
|
|
||||||
} else {
|
|
||||||
var categoryApps = AppSearchService.getAppsInCategory(
|
|
||||||
selectedCategory)
|
|
||||||
if (categoryApps.length > 0) {
|
|
||||||
var allSearchResults = AppSearchService.searchApplications(
|
|
||||||
searchQuery)
|
|
||||||
var categoryNames = new Set(categoryApps.map(app => {
|
|
||||||
return app.name
|
|
||||||
}))
|
|
||||||
apps = allSearchResults.filter(searchApp => {
|
|
||||||
return categoryNames.has(
|
|
||||||
searchApp.name)
|
|
||||||
}).slice(0, maxResults)
|
|
||||||
} else {
|
|
||||||
apps = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (searchQuery.length === 0)
|
|
||||||
apps = apps.sort(function (a, b) {
|
|
||||||
var aId = a.id || (a.execString || a.exec || "")
|
|
||||||
var bId = b.id || (b.execString || b.exec || "")
|
|
||||||
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
|
||||||
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
|
||||||
if (aUsage !== bUsage)
|
|
||||||
return bUsage - aUsage
|
|
||||||
|
|
||||||
return (a.name || "").localeCompare(b.name || "")
|
|
||||||
})
|
|
||||||
|
|
||||||
apps.forEach(app => {
|
|
||||||
if (app)
|
|
||||||
filteredModel.append({
|
|
||||||
"name": app.name || "",
|
|
||||||
"exec": app.execString || "",
|
|
||||||
"icon": app.icon
|
|
||||||
|| "application-x-executable",
|
|
||||||
"comment": app.comment || "",
|
|
||||||
"categories": app.categories
|
|
||||||
|| [],
|
|
||||||
"desktopEntry": app
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (filteredModel.count > 0) {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
if (viewMode === "grid") {
|
|
||||||
var newIndex = Math.min(selectedIndex + gridColumns,
|
|
||||||
filteredModel.count - 1)
|
|
||||||
selectedIndex = newIndex
|
|
||||||
} else {
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1,
|
|
||||||
filteredModel.count - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (filteredModel.count > 0) {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
if (viewMode === "grid") {
|
|
||||||
var newIndex = Math.max(selectedIndex - gridColumns, 0)
|
|
||||||
selectedIndex = newIndex
|
|
||||||
} else {
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNextInRow() {
|
|
||||||
if (filteredModel.count > 0 && viewMode === "grid") {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPreviousInRow() {
|
|
||||||
if (filteredModel.count > 0 && viewMode === "grid") {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchSelected() {
|
|
||||||
if (filteredModel.count > 0 && selectedIndex >= 0
|
|
||||||
&& selectedIndex < filteredModel.count) {
|
|
||||||
var selectedApp = filteredModel.get(selectedIndex)
|
|
||||||
launchApp(selectedApp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchApp(appData) {
|
|
||||||
if (!appData)
|
|
||||||
return
|
|
||||||
|
|
||||||
appData.desktopEntry.execute()
|
|
||||||
appLaunched(appData)
|
|
||||||
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCategory(category) {
|
|
||||||
selectedCategory = category
|
|
||||||
categorySelected(category)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setViewMode(mode) {
|
|
||||||
viewMode = mode
|
|
||||||
viewModeSelected(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchQueryChanged: {
|
|
||||||
if (debounceSearch)
|
|
||||||
searchDebounceTimer.restart()
|
|
||||||
else
|
|
||||||
updateFilteredModel()
|
|
||||||
}
|
|
||||||
onSelectedCategoryChanged: updateFilteredModel()
|
|
||||||
onAppUsageRankingChanged: updateFilteredModel()
|
|
||||||
on_WatchApplicationsChanged: updateFilteredModel()
|
|
||||||
Component.onCompleted: {
|
|
||||||
updateFilteredModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: filteredModel
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: searchDebounceTimer
|
|
||||||
|
|
||||||
interval: root.debounceInterval
|
|
||||||
repeat: false
|
|
||||||
onTriggered: updateFilteredModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var categories: []
|
|
||||||
property string selectedCategory: "All"
|
|
||||||
property bool compact: false // For different layout styles
|
|
||||||
|
|
||||||
signal categorySelected(string category)
|
|
||||||
|
|
||||||
height: compact ? 36 : (72 + Theme.spacingS) // Single row vs two rows
|
|
||||||
|
|
||||||
Row {
|
|
||||||
visible: compact
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: categories.slice(0, Math.min(categories.length,
|
|
||||||
8)) // Limit for space
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: 36
|
|
||||||
width: (parent.width - (Math.min(
|
|
||||||
categories.length,
|
|
||||||
8) - 1) * Theme.spacingS) / Math.min(
|
|
||||||
categories.length, 8)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
|
||||||
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b,
|
|
||||||
0.3)
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
selectedCategory = modelData
|
|
||||||
categorySelected(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
visible: !compact
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
property var firstRowCategories: categories.slice(
|
|
||||||
0, Math.min(4,
|
|
||||||
categories.length))
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: parent.firstRowCategories
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: 36
|
|
||||||
width: (parent.width - (parent.firstRowCategories.length - 1)
|
|
||||||
* Theme.spacingS) / parent.firstRowCategories.length
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
|
||||||
border.color: selectedCategory
|
|
||||||
=== modelData ? "transparent" : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
selectedCategory = modelData
|
|
||||||
categorySelected(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
property var secondRowCategories: categories.slice(
|
|
||||||
4, categories.length)
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: secondRowCategories.length > 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: parent.secondRowCategories
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: 36
|
|
||||||
width: (parent.width - (parent.secondRowCategories.length - 1)
|
|
||||||
* Theme.spacingS) / parent.secondRowCategories.length
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
|
||||||
border.color: selectedCategory
|
|
||||||
=== modelData ? "transparent" : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
selectedCategory = modelData
|
|
||||||
categorySelected(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: calendarGrid
|
|
||||||
|
|
||||||
property date displayDate: new Date()
|
|
||||||
property date selectedDate: new Date()
|
|
||||||
|
|
||||||
function loadEventsForMonth() {
|
|
||||||
if (!CalendarService || !CalendarService.khalAvailable)
|
|
||||||
return
|
|
||||||
|
|
||||||
let firstDay = new Date(displayDate.getFullYear(),
|
|
||||||
displayDate.getMonth(), 1)
|
|
||||||
let dayOfWeek = firstDay.getDay()
|
|
||||||
let startDate = new Date(firstDay)
|
|
||||||
startDate.setDate(startDate.getDate(
|
|
||||||
) - dayOfWeek - 7) // Extra week padding
|
|
||||||
let lastDay = new Date(displayDate.getFullYear(),
|
|
||||||
displayDate.getMonth() + 1, 0)
|
|
||||||
let endDate = new Date(lastDay)
|
|
||||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay(
|
|
||||||
)) + 7) // Extra week padding
|
|
||||||
CalendarService.loadEvents(startDate, endDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
onDisplayDateChanged: {
|
|
||||||
loadEventsForMonth()
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
loadEventsForMonth()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onKhalAvailableChanged() {
|
|
||||||
if (CalendarService && CalendarService.khalAvailable)
|
|
||||||
loadEventsForMonth()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: CalendarService
|
|
||||||
enabled: CalendarService !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "chevron_left"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: prevMonthArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
let newDate = new Date(displayDate)
|
|
||||||
newDate.setMonth(newDate.getMonth() - 1)
|
|
||||||
displayDate = newDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width - 80
|
|
||||||
height: 40
|
|
||||||
text: displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "chevron_right"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nextMonthArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
let newDate = new Date(displayDate)
|
|
||||||
newDate.setMonth(newDate.getMonth() + 1)
|
|
||||||
displayDate = newDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
var days = []
|
|
||||||
var locale = Qt.locale()
|
|
||||||
for (var i = 0; i < 7; i++) {
|
|
||||||
var date = new Date(2024, 0, 7 + i)
|
|
||||||
days.push(locale.dayName(i, Locale.ShortFormat))
|
|
||||||
}
|
|
||||||
return days
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width / 7
|
|
||||||
height: 32
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid {
|
|
||||||
property date firstDay: {
|
|
||||||
let date = new Date(displayDate.getFullYear(),
|
|
||||||
displayDate.getMonth(), 1)
|
|
||||||
let dayOfWeek = date.getDay()
|
|
||||||
date.setDate(date.getDate() - dayOfWeek)
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 200 // Fixed height for calendar
|
|
||||||
columns: 7
|
|
||||||
rows: 6
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: 42
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property date dayDate: {
|
|
||||||
let date = new Date(parent.firstDay)
|
|
||||||
date.setDate(date.getDate() + index)
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
property bool isCurrentMonth: dayDate.getMonth(
|
|
||||||
) === displayDate.getMonth()
|
|
||||||
property bool isToday: dayDate.toDateString(
|
|
||||||
) === new Date().toDateString()
|
|
||||||
property bool isSelected: dayDate.toDateString(
|
|
||||||
) === selectedDate.toDateString()
|
|
||||||
|
|
||||||
width: parent.width / 7
|
|
||||||
height: parent.height / 6
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - 4
|
|
||||||
height: parent.height - 4
|
|
||||||
color: isSelected ? Theme.primary : isToday ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: dayDate.getDate()
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
|
||||||
font.weight: isToday
|
|
||||||
|| isSelected ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: eventIndicator
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: parent.radius
|
|
||||||
visible: CalendarService
|
|
||||||
&& CalendarService.khalAvailable
|
|
||||||
&& CalendarService.hasEventsForDate(dayDate)
|
|
||||||
opacity: {
|
|
||||||
if (isSelected)
|
|
||||||
return 0.9
|
|
||||||
else if (isToday)
|
|
||||||
return 0.8
|
|
||||||
else
|
|
||||||
return 0.6
|
|
||||||
}
|
|
||||||
|
|
||||||
gradient: Gradient {
|
|
||||||
GradientStop {
|
|
||||||
position: 0.89
|
|
||||||
color: "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
position: 0.9
|
|
||||||
color: {
|
|
||||||
if (isSelected)
|
|
||||||
return Qt.lighter(Theme.primary, 1.3)
|
|
||||||
else if (isToday)
|
|
||||||
return Theme.primary
|
|
||||||
else
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
position: 1
|
|
||||||
color: {
|
|
||||||
if (isSelected)
|
|
||||||
return Qt.lighter(Theme.primary, 1.3)
|
|
||||||
else if (isToday)
|
|
||||||
return Theme.primary
|
|
||||||
else
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dayArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
selectedDate = dayDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.CentcomCenter
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property bool hasActiveMedia: MprisController.activePlayer !== null
|
|
||||||
property bool calendarVisible: false
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property real triggerX: (Screen.width - 480) / 2
|
|
||||||
property real triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4
|
|
||||||
property real triggerWidth: 80
|
|
||||||
property string triggerSection: "center"
|
|
||||||
property var triggerScreen: null
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
triggerX = x
|
|
||||||
triggerY = y
|
|
||||||
triggerWidth = width
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: calendarVisible || closeTimer.running
|
|
||||||
screen: triggerScreen
|
|
||||||
onCalendarVisibleChanged: {
|
|
||||||
if (calendarVisible) {
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
Qt.callLater(() => {
|
|
||||||
calendarGrid.loadEventsForMonth()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
shouldBeVisible = false
|
|
||||||
closeTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
interval: Theme.mediumDuration + 50
|
|
||||||
onTriggered: {
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && calendarGrid)
|
|
||||||
calendarGrid.loadEventsForMonth()
|
|
||||||
}
|
|
||||||
implicitWidth: 480
|
|
||||||
implicitHeight: 600
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: mainContainer
|
|
||||||
|
|
||||||
readonly property real targetWidth: Math.min(
|
|
||||||
(root.screen ? root.screen.width : Screen.width)
|
|
||||||
* 0.9, 600)
|
|
||||||
|
|
||||||
function calculateWidth() {
|
|
||||||
let baseWidth = 320
|
|
||||||
if (leftWidgets.hasAnyWidgets)
|
|
||||||
return Math.min(parent.width * 0.9, 600)
|
|
||||||
|
|
||||||
return Math.min(parent.width * 0.7, 400)
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateHeight() {
|
|
||||||
let contentHeight = Theme.spacingM * 2
|
|
||||||
// margins
|
|
||||||
let widgetHeight = 160
|
|
||||||
widgetHeight += 140 + Theme.spacingM
|
|
||||||
let calendarHeight = 300
|
|
||||||
let mainRowHeight = Math.max(widgetHeight, calendarHeight)
|
|
||||||
contentHeight += mainRowHeight + Theme.spacingM
|
|
||||||
if (CalendarService && CalendarService.khalAvailable) {
|
|
||||||
let hasEvents = events.selectedDateEvents
|
|
||||||
&& events.selectedDateEvents.length > 0
|
|
||||||
let eventsHeight = hasEvents ? Math.min(
|
|
||||||
300,
|
|
||||||
80 + events.selectedDateEvents.length * 60) : 120
|
|
||||||
contentHeight += eventsHeight
|
|
||||||
} else {
|
|
||||||
contentHeight -= Theme.spacingM
|
|
||||||
}
|
|
||||||
return Math.min(contentHeight, parent.height * 0.9)
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real calculatedX: {
|
|
||||||
var screenWidth = root.screen ? root.screen.width : Screen.width
|
|
||||||
if (root.triggerSection === "center") {
|
|
||||||
return (screenWidth - targetWidth) / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
var centerX = root.triggerX + (root.triggerWidth / 2) - (targetWidth / 2)
|
|
||||||
|
|
||||||
if (centerX >= Theme.spacingM
|
|
||||||
&& centerX + targetWidth <= screenWidth - Theme.spacingM) {
|
|
||||||
return centerX
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centerX < Theme.spacingM) {
|
|
||||||
return Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
|
|
||||||
return screenWidth - targetWidth - Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
return centerX
|
|
||||||
}
|
|
||||||
|
|
||||||
width: targetWidth
|
|
||||||
height: calculateHeight()
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
layer.enabled: true
|
|
||||||
opacity: shouldBeVisible ? 1 : 0
|
|
||||||
scale: shouldBeVisible ? 1 : 0.9
|
|
||||||
x: calculatedX
|
|
||||||
y: root.triggerY
|
|
||||||
onOpacityChanged: {
|
|
||||||
if (opacity === 1)
|
|
||||||
Qt.callLater(() => {
|
|
||||||
height = calculateHeight()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onEventsByDateChanged() {
|
|
||||||
if (mainContainer.opacity === 1)
|
|
||||||
mainContainer.height = mainContainer.calculateHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKhalAvailableChanged() {
|
|
||||||
if (mainContainer.opacity === 1)
|
|
||||||
mainContainer.height = mainContainer.calculateHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: CalendarService
|
|
||||||
enabled: CalendarService !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onSelectedDateEventsChanged() {
|
|
||||||
if (mainContainer.opacity === 1)
|
|
||||||
mainContainer.height = mainContainer.calculateHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: events
|
|
||||||
enabled: events !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
|
|
||||||
Theme.surfaceTint.b, 0.04)
|
|
||||||
radius: parent.radius
|
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
|
||||||
running: 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 {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
calendarVisible = false
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
// Don't handle other keys - let them bubble up to modals
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: {
|
|
||||||
let widgetHeight = 160
|
|
||||||
// Media widget
|
|
||||||
widgetHeight += 140 + Theme.spacingM // Weather/SystemInfo widget with spacing
|
|
||||||
let calendarHeight = 300
|
|
||||||
// Calendar
|
|
||||||
return Math.max(widgetHeight, calendarHeight)
|
|
||||||
}
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: leftWidgets
|
|
||||||
|
|
||||||
property bool hasAnyWidgets: true
|
|
||||||
|
|
||||||
width: hasAnyWidgets ? parent.width
|
|
||||||
* 0.42 : 0 // Slightly narrower for better proportions
|
|
||||||
height: childrenRect.height
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: hasAnyWidgets
|
|
||||||
anchors.top: parent.top
|
|
||||||
|
|
||||||
MediaPlayer {
|
|
||||||
width: parent.width
|
|
||||||
height: 160
|
|
||||||
}
|
|
||||||
|
|
||||||
Weather {
|
|
||||||
width: parent.width
|
|
||||||
height: 140
|
|
||||||
visible: SettingsData.weatherEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemInfo {
|
|
||||||
width: parent.width
|
|
||||||
height: 140
|
|
||||||
visible: !SettingsData.weatherEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width
|
|
||||||
- Theme.spacingM : parent.width
|
|
||||||
height: parent.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.2)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
CalendarGrid {
|
|
||||||
id: calendarGrid
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Events {
|
|
||||||
id: events
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
selectedDate: calendarGrid.selectedDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durMed
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durMed
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 4
|
|
||||||
shadowBlur: 0.5
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
shadowOpacity: 0.15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
z: -1
|
|
||||||
enabled: shouldBeVisible
|
|
||||||
onClicked: function (mouse) {
|
|
||||||
var localPos = mapToItem(mainContainer, mouse.x, mouse.y)
|
|
||||||
if (localPos.x < 0 || localPos.x > mainContainer.width
|
|
||||||
|| localPos.y < 0 || localPos.y > mainContainer.height)
|
|
||||||
calendarVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: events
|
|
||||||
|
|
||||||
property date selectedDate: new Date()
|
|
||||||
property var selectedDateEvents: []
|
|
||||||
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
|
|
||||||
property bool shouldShow: CalendarService && CalendarService.khalAvailable
|
|
||||||
|
|
||||||
function updateSelectedDateEvents() {
|
|
||||||
if (CalendarService && CalendarService.khalAvailable) {
|
|
||||||
let events = CalendarService.getEventsForDate(selectedDate)
|
|
||||||
selectedDateEvents = events
|
|
||||||
} else {
|
|
||||||
selectedDateEvents = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelectedDateEventsChanged: {
|
|
||||||
eventsList.model = selectedDateEvents
|
|
||||||
}
|
|
||||||
width: parent.width
|
|
||||||
height: shouldShow ? (hasEvents ? Math.min(
|
|
||||||
300,
|
|
||||||
80 + selectedDateEvents.length * 60) : 120) : 0
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.12)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
visible: shouldShow
|
|
||||||
layer.enabled: true
|
|
||||||
Component.onCompleted: {
|
|
||||||
updateSelectedDateEvents()
|
|
||||||
}
|
|
||||||
onSelectedDateChanged: {
|
|
||||||
updateSelectedDateEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onEventsByDateChanged() {
|
|
||||||
updateSelectedDateEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKhalAvailableChanged() {
|
|
||||||
updateSelectedDateEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: CalendarService
|
|
||||||
enabled: CalendarService !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: headerRow
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "event"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • "
|
|
||||||
+ (selectedDateEvents.length
|
|
||||||
=== 1 ? "1 event" : selectedDateEvents.length
|
|
||||||
+ " events")) : Qt.formatDate(
|
|
||||||
selectedDate, "MMM d")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: !hasEvents
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "event_busy"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.3)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No events"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: eventsList
|
|
||||||
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
visible: opacity > 0
|
|
||||||
opacity: hasEvents ? 1 : 0
|
|
||||||
clip: true
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
interactive: true
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
|
||||||
pressDelay: 0
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
property real momentum: 0
|
|
||||||
onWheel: event => {
|
|
||||||
if (event.pixelDelta.y !== 0) {
|
|
||||||
// Touchpad with pixel delta
|
|
||||||
momentum = event.pixelDelta.y * 1.8
|
|
||||||
} else {
|
|
||||||
// Mouse wheel with angle delta
|
|
||||||
momentum = (event.angleDelta.y / 120)
|
|
||||||
* (60 * 2.5) // ~2.5 items per wheel step
|
|
||||||
}
|
|
||||||
|
|
||||||
let newY = parent.contentY - momentum
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
momentum *= 0.92 // Decay for smooth momentum
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: eventsList.contentHeight
|
|
||||||
> eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: eventsList.width
|
|
||||||
height: eventContent.implicitHeight + Theme.spacingM
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (modelData.url && eventMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
else if (eventMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.06)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.06)
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
if (modelData.url && eventMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.3)
|
|
||||||
else if (eventMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.15)
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 4
|
|
||||||
height: parent.height - 8
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 4
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
radius: 2
|
|
||||||
color: Theme.primary
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: eventContent
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingL + 4
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: modelData.title
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
maximumLineCount: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Math.max(timeRow.height, locationRow.height)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: timeRow
|
|
||||||
|
|
||||||
spacing: 4
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "schedule"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (modelData.allDay) {
|
|
||||||
return "All day"
|
|
||||||
} else {
|
|
||||||
let timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
|
||||||
let startTime = Qt.formatTime(
|
|
||||||
modelData.start, timeFormat)
|
|
||||||
if (modelData.start.toDateString(
|
|
||||||
) !== modelData.end.toDateString(
|
|
||||||
) || modelData.start.getTime(
|
|
||||||
) !== modelData.end.getTime())
|
|
||||||
return startTime + " – " + Qt.formatTime(
|
|
||||||
modelData.end, timeFormat)
|
|
||||||
|
|
||||||
return startTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: locationRow
|
|
||||||
|
|
||||||
spacing: 4
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: modelData.location !== ""
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "location_on"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.location
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
maximumLineCount: 1
|
|
||||||
width: Math.min(implicitWidth, 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: eventMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: modelData.url !== ""
|
|
||||||
onClicked: {
|
|
||||||
if (modelData.url && modelData.url !== "") {
|
|
||||||
if (Qt.openUrlExternally(modelData.url) === false)
|
|
||||||
console.warn("Failed to open URL: " + modelData.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.25
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
|
||||||
shadowOpacity: 0.1
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,502 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: mediaPlayer
|
|
||||||
|
|
||||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
|
||||||
property string lastValidTitle: ""
|
|
||||||
property string lastValidArtist: ""
|
|
||||||
property string lastValidAlbum: ""
|
|
||||||
property string lastValidArtUrl: ""
|
|
||||||
property real currentPosition: activePlayer
|
|
||||||
&& activePlayer.positionSupported ? activePlayer.position : 0
|
|
||||||
property real displayPosition: currentPosition
|
|
||||||
|
|
||||||
function ratio() {
|
|
||||||
if (!activePlayer || activePlayer.length <= 0) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
let calculatedRatio = displayPosition / activePlayer.length
|
|
||||||
return Math.max(0, Math.min(1, calculatedRatio))
|
|
||||||
}
|
|
||||||
|
|
||||||
onActivePlayerChanged: {
|
|
||||||
if (activePlayer && activePlayer.positionSupported) {
|
|
||||||
currentPosition = Qt.binding(() => activePlayer.position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: positionTimer
|
|
||||||
interval: 300
|
|
||||||
running: activePlayer
|
|
||||||
&& activePlayer.playbackState === MprisPlaybackState.Playing
|
|
||||||
&& !progressMouseArea.isSeeking
|
|
||||||
repeat: true
|
|
||||||
onTriggered: activePlayer && activePlayer.positionSupported && activePlayer.positionChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: cleanupTimer
|
|
||||||
|
|
||||||
interval: 2000
|
|
||||||
running: !activePlayer
|
|
||||||
onTriggered: {
|
|
||||||
lastValidTitle = ""
|
|
||||||
lastValidArtist = ""
|
|
||||||
lastValidAlbum = ""
|
|
||||||
lastValidArtUrl = ""
|
|
||||||
currentPosition = 0
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: (!activePlayer && !lastValidTitle)
|
|
||||||
|| (activePlayer && activePlayer.trackTitle === ""
|
|
||||||
&& lastValidTitle === "")
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "music_note"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No Media Playing"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: activePlayer && activePlayer.trackTitle !== ""
|
|
||||||
|| lastValidTitle !== ""
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 60
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: albumArt
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: activePlayer && activePlayer.trackArtUrl
|
|
||||||
|| lastValidArtUrl || ""
|
|
||||||
onSourceChanged: {
|
|
||||||
if (activePlayer && activePlayer.trackArtUrl
|
|
||||||
&& albumArt.status !== Image.Error)
|
|
||||||
lastValidArtUrl = activePlayer.trackArtUrl
|
|
||||||
}
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
smooth: true
|
|
||||||
cache: true
|
|
||||||
asynchronous: true
|
|
||||||
onStatusChanged: {
|
|
||||||
if (status === Image.Error) {
|
|
||||||
console.warn("Failed to load album art:", source)
|
|
||||||
source = ""
|
|
||||||
if (activePlayer && activePlayer.trackArtUrl === source) {
|
|
||||||
lastValidArtUrl = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: albumArt.status !== Image.Ready || !albumArt.source
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "album"
|
|
||||||
size: 28
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 60 - Theme.spacingM
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: activePlayer && activePlayer.trackTitle
|
|
||||||
|| lastValidTitle || "Unknown Track"
|
|
||||||
onTextChanged: {
|
|
||||||
if (activePlayer && activePlayer.trackTitle)
|
|
||||||
lastValidTitle = activePlayer.trackTitle
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: activePlayer && activePlayer.trackArtist
|
|
||||||
|| lastValidArtist || "Unknown Artist"
|
|
||||||
onTextChanged: {
|
|
||||||
if (activePlayer && activePlayer.trackArtist)
|
|
||||||
lastValidArtist = activePlayer.trackArtist
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.8)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: activePlayer && activePlayer.trackAlbum
|
|
||||||
|| lastValidAlbum || ""
|
|
||||||
onTextChanged: {
|
|
||||||
if (activePlayer && activePlayer.trackAlbum)
|
|
||||||
lastValidAlbum = activePlayer.trackAlbum
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: progressBarContainer
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: progressBarBackground
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 6
|
|
||||||
radius: 3
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
visible: activePlayer !== null
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: progressFill
|
|
||||||
|
|
||||||
height: parent.height
|
|
||||||
radius: parent.radius
|
|
||||||
color: Theme.primary
|
|
||||||
width: Math.max(0, Math.min(parent.width,
|
|
||||||
parent.width * ratio()))
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: progressHandle
|
|
||||||
|
|
||||||
width: 12
|
|
||||||
height: 12
|
|
||||||
radius: 6
|
|
||||||
color: Theme.primary
|
|
||||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
|
||||||
border.width: 1
|
|
||||||
x: Math.max(0, Math.min(parent.width - width,
|
|
||||||
progressFill.width - width / 2))
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: activePlayer && activePlayer.length > 0
|
|
||||||
scale: progressMouseArea.containsMouse
|
|
||||||
|| progressMouseArea.pressed ? 1.2 : 1
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 150
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: progressMouseArea
|
|
||||||
|
|
||||||
property bool isSeeking: false
|
|
||||||
property real pendingSeekPosition: -1
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: seekDebounceTimer
|
|
||||||
interval: 150
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
|
||||||
let clampedPosition = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
|
|
||||||
activePlayer.position = clampedPosition
|
|
||||||
progressMouseArea.pendingSeekPosition = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: activePlayer && activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek
|
|
||||||
preventStealing: true
|
|
||||||
onPressed: function (mouse) {
|
|
||||||
isSeeking = true
|
|
||||||
if (activePlayer && activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
mouse.x / progressBarBackground.width))
|
|
||||||
pendingSeekPosition = ratio * activePlayer.length
|
|
||||||
displayPosition = pendingSeekPosition
|
|
||||||
seekDebounceTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
isSeeking = false
|
|
||||||
seekDebounceTimer.stop()
|
|
||||||
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
|
||||||
let clampedPosition = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
|
||||||
activePlayer.position = clampedPosition
|
|
||||||
pendingSeekPosition = -1
|
|
||||||
}
|
|
||||||
displayPosition = Qt.binding(() => currentPosition)
|
|
||||||
}
|
|
||||||
onPositionChanged: function (mouse) {
|
|
||||||
if (pressed && isSeeking && activePlayer
|
|
||||||
&& activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
mouse.x / progressBarBackground.width))
|
|
||||||
pendingSeekPosition = ratio * activePlayer.length
|
|
||||||
displayPosition = pendingSeekPosition
|
|
||||||
seekDebounceTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClicked: function (mouse) {
|
|
||||||
if (activePlayer && activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
mouse.x / progressBarBackground.width))
|
|
||||||
activePlayer.position = ratio * activePlayer.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: progressGlobalMouseArea
|
|
||||||
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: mediaPlayer.width
|
|
||||||
height: mediaPlayer.height
|
|
||||||
enabled: progressMouseArea.isSeeking
|
|
||||||
visible: false
|
|
||||||
preventStealing: true
|
|
||||||
onPositionChanged: function (mouse) {
|
|
||||||
if (progressMouseArea.isSeeking && activePlayer
|
|
||||||
&& activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
let globalPos = mapToItem(progressBarBackground,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
globalPos.x / progressBarBackground.width))
|
|
||||||
progressMouseArea.pendingSeekPosition = ratio * activePlayer.length
|
|
||||||
displayPosition = progressMouseArea.pendingSeekPosition
|
|
||||||
seekDebounceTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
progressMouseArea.isSeeking = false
|
|
||||||
seekDebounceTimer.stop()
|
|
||||||
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
|
||||||
let clampedPosition = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
|
|
||||||
activePlayer.position = clampedPosition
|
|
||||||
progressMouseArea.pendingSeekPosition = -1
|
|
||||||
}
|
|
||||||
displayPosition = Qt.binding(() => currentPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
visible: activePlayer !== null
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
height: parent.height
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: prevBtnArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_previous"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: prevBtnArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!activePlayer)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (activePlayer.position > 8
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
activePlayer.position = 0
|
|
||||||
} else {
|
|
||||||
activePlayer.previous()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Theme.primary
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: activePlayer && activePlayer.playbackState
|
|
||||||
=== MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
|
||||||
size: 20
|
|
||||||
color: Theme.background
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer
|
|
||||||
&& activePlayer.togglePlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: nextBtnArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_next"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nextBtnArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer && activePlayer.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.5
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
|
||||||
shadowOpacity: 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: DgopService
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
SystemLogo {
|
|
||||||
width: 48
|
|
||||||
height: 48
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 48 - Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.hostname
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.distribution + " • " + DgopService.architecture
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Uptime " + formatUptime(UserInfoService.uptime)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Load: " + DgopService.loadAverage
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.processCount + " proc, " + DgopService.threadCount + " threads"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.8)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUptime(uptime) {
|
|
||||||
if (!uptime)
|
|
||||||
return "0m"
|
|
||||||
|
|
||||||
// Parse the uptime string - handle formats like "1 week, 4 days, 3:45" or "4 days, 3:45" or "3:45"
|
|
||||||
var uptimeStr = uptime.toString().trim()
|
|
||||||
|
|
||||||
// Check for weeks and days - need to add them together
|
|
||||||
var weekMatch = uptimeStr.match(/(\d+)\s+weeks?/)
|
|
||||||
var dayMatch = uptimeStr.match(/(\d+)\s+days?/)
|
|
||||||
|
|
||||||
if (weekMatch) {
|
|
||||||
var weeks = parseInt(weekMatch[1])
|
|
||||||
var totalDays = weeks * 7
|
|
||||||
if (dayMatch) {
|
|
||||||
var days = parseInt(dayMatch[1])
|
|
||||||
totalDays += days
|
|
||||||
}
|
|
||||||
return totalDays + "d"
|
|
||||||
} else if (dayMatch) {
|
|
||||||
var days = parseInt(dayMatch[1])
|
|
||||||
return days + "d"
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's just hours:minutes, show the largest unit
|
|
||||||
var timeMatch = uptimeStr.match(/(\d+):(\d+)/)
|
|
||||||
if (timeMatch) {
|
|
||||||
var hours = parseInt(timeMatch[1])
|
|
||||||
var minutes = parseInt(timeMatch[2])
|
|
||||||
if (hours > 0) {
|
|
||||||
return hours + "h"
|
|
||||||
} else {
|
|
||||||
return minutes + "m"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback - return as is but truncated
|
|
||||||
return uptimeStr.length > 8 ? uptimeStr.substring(0,
|
|
||||||
8) + "…" : uptimeStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: weather
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: WeatherService
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: !WeatherService.weather.available
|
|
||||||
|| WeatherService.weather.temp === 0
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "cloud_off"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No Weather Data"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: WeatherService.weather.available
|
|
||||||
&& WeatherService.weather.temp !== 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: refreshButton
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.3)
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.rightMargin: -Theme.spacingS
|
|
||||||
anchors.topMargin: -Theme.spacingS
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(
|
|
||||||
WeatherService.weather.wCode)
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp)
|
|
||||||
+ "°" + (SettingsData.useFahrenheit ? "F" : "C")
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Light
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (WeatherService.weather.available)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid {
|
|
||||||
columns: 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "humidity_low"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.humidity ? WeatherService.weather.humidity
|
|
||||||
+ "%" : "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "air"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.wind || "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "wb_twilight"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.sunrise || "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.sunset || "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.5
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
|
||||||
shadowOpacity: 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,781 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ControlCenter
|
|
||||||
import qs.Modules.ControlCenter.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Details
|
|
||||||
import qs.Modules.ControlCenter.Details 1.0 as Details
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankPopout {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string expandedSection: ""
|
|
||||||
property bool powerOptionsExpanded: false
|
|
||||||
property string triggerSection: "right"
|
|
||||||
property var triggerScreen: null
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
triggerX = x
|
|
||||||
triggerY = y
|
|
||||||
triggerWidth = width
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
}
|
|
||||||
|
|
||||||
function openWithSection(section) {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
close()
|
|
||||||
} else {
|
|
||||||
expandedSection = section
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSection(section) {
|
|
||||||
if (expandedSection === section) {
|
|
||||||
expandedSection = ""
|
|
||||||
} else {
|
|
||||||
expandedSection = section
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signal powerActionRequested(string action, string title, string message)
|
|
||||||
signal lockRequested
|
|
||||||
|
|
||||||
popupWidth: 550
|
|
||||||
popupHeight: Math.min(Screen.height - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
|
|
||||||
triggerX: Screen.width - 600 - Theme.spacingL
|
|
||||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
|
||||||
triggerWidth: 80
|
|
||||||
positioning: "center"
|
|
||||||
WlrLayershell.namespace: "quickshell-controlcenter"
|
|
||||||
screen: triggerScreen
|
|
||||||
shouldBeVisible: false
|
|
||||||
visible: shouldBeVisible
|
|
||||||
|
|
||||||
onShouldBeVisibleChanged: {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
|
|
||||||
if (UserInfoService)
|
|
||||||
UserInfoService.getUptime()
|
|
||||||
} else {
|
|
||||||
NetworkService.autoRefreshEnabled = false
|
|
||||||
if (BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.discovering)
|
|
||||||
BluetoothService.adapter.discovering = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
implicitHeight: controlContent.implicitHeight
|
|
||||||
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: controlContent
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
implicitHeight: mainColumn.implicitHeight + 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
|
|
||||||
antialiasing: true
|
|
||||||
smooth: true
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
Qt.callLater(function () {
|
|
||||||
controlContent.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
width: parent.width - Theme.spacingL * 2
|
|
||||||
x: Theme.spacingL
|
|
||||||
y: Theme.spacingL
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 90
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha() * 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: avatarContainer
|
|
||||||
|
|
||||||
property bool hasImage: profileImageLoader.status === Image.Ready
|
|
||||||
|
|
||||||
width: 64
|
|
||||||
height: 64
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 1
|
|
||||||
visible: parent.hasImage
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: profileImageLoader
|
|
||||||
|
|
||||||
source: {
|
|
||||||
if (PortalService.profileImage === "")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
if (PortalService.profileImage.startsWith(
|
|
||||||
"/"))
|
|
||||||
return "file://" + PortalService.profileImage
|
|
||||||
|
|
||||||
return PortalService.profileImage
|
|
||||||
}
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
mipmap: true
|
|
||||||
cache: true
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
source: profileImageLoader
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: circularMask
|
|
||||||
visible: avatarContainer.hasImage
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: circularMask
|
|
||||||
|
|
||||||
width: 64 - 10
|
|
||||||
height: 64 - 10
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: Theme.primary
|
|
||||||
visible: !parent.hasImage
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "person"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Theme.primaryText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "warning"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Theme.primaryText
|
|
||||||
visible: PortalService.profileImage !== ""
|
|
||||||
&& profileImageLoader.status === Image.Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: UserInfoService.fullName
|
|
||||||
|| UserInfoService.username || "User"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (UserInfoService.uptime
|
|
||||||
|| "Unknown")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 40
|
|
||||||
iconName: "lock"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
backgroundColor: Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
hoverColor: Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
onClicked: {
|
|
||||||
root.close()
|
|
||||||
root.lockRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 40
|
|
||||||
iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
backgroundColor: Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
hoverColor: Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
onClicked: {
|
|
||||||
root.powerOptionsExpanded = !root.powerOptionsExpanded
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 40
|
|
||||||
iconName: "settings"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
backgroundColor: Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
hoverColor: Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
onClicked: {
|
|
||||||
root.close()
|
|
||||||
settingsModal.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
implicitHeight: root.powerOptionsExpanded ? 60 : 0
|
|
||||||
height: implicitHeight
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha() * 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: root.powerOptionsExpanded ? 1 : 0
|
|
||||||
opacity: root.powerOptionsExpanded ? 1 : 0
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
visible: root.powerOptionsExpanded
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 34
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: logoutButton.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Logout"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = false
|
|
||||||
root.close()
|
|
||||||
root.powerActionRequested(
|
|
||||||
"logout", "Logout",
|
|
||||||
"Are you sure you want to logout?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 34
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: rebootButton.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "restart_alt"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Restart"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = false
|
|
||||||
root.close()
|
|
||||||
root.powerActionRequested(
|
|
||||||
"reboot", "Restart",
|
|
||||||
"Are you sure you want to restart?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 34
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: suspendButton.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Suspend"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = false
|
|
||||||
root.close()
|
|
||||||
root.powerActionRequested(
|
|
||||||
"suspend", "Suspend",
|
|
||||||
"Are you sure you want to suspend?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 34
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: shutdownButton.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power_settings_new"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Shutdown"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: shutdownButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = false
|
|
||||||
root.close()
|
|
||||||
root.powerActionRequested(
|
|
||||||
"poweroff", "Shutdown",
|
|
||||||
"Are you sure you want to shutdown?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: audioSliderRow.implicitHeight
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: audioSliderRow
|
|
||||||
x: -Theme.spacingS
|
|
||||||
width: parent.width + Theme.spacingS * 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
AudioSliderRow {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: parent.height
|
|
||||||
|
|
||||||
BrightnessSliderRow {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
x: -Theme.spacingS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
NetworkPill {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
expanded: root.expandedSection === "network"
|
|
||||||
onClicked: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
if (NetworkService.ethernetConnected && !NetworkService.wifiEnabled) {
|
|
||||||
NetworkService.toggleWifiRadio()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
root.toggleSection("network")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi") {
|
|
||||||
if (NetworkService.ethernetConnected) {
|
|
||||||
NetworkService.toggleWifiRadio()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
NetworkService.disconnectWifi()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!NetworkService.wifiEnabled) {
|
|
||||||
NetworkService.toggleWifiRadio()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (NetworkService.wifiEnabled && NetworkService.networkStatus === "disconnected") {
|
|
||||||
root.toggleSection("network")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExpandClicked: root.toggleSection("network")
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothPill {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
expanded: root.expandedSection === "bluetooth"
|
|
||||||
onClicked: {
|
|
||||||
if (BluetoothService.adapter)
|
|
||||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
|
||||||
}
|
|
||||||
onExpandClicked: root.toggleSection("bluetooth")
|
|
||||||
visible: BluetoothService.available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
active: root.expandedSection === "network" || root.expandedSection === "bluetooth"
|
|
||||||
visible: active
|
|
||||||
sourceComponent: DetailView {
|
|
||||||
width: parent.width
|
|
||||||
isVisible: true
|
|
||||||
title: {
|
|
||||||
switch (root.expandedSection) {
|
|
||||||
case "network": return "Network Settings"
|
|
||||||
case "bluetooth": return "Bluetooth Settings"
|
|
||||||
default: return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content: {
|
|
||||||
switch (root.expandedSection) {
|
|
||||||
case "network": return networkDetailComponent
|
|
||||||
case "bluetooth": return bluetoothDetailComponent
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contentHeight: 250
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
AudioOutputPill {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
expanded: root.expandedSection === "audio_output"
|
|
||||||
onClicked: {
|
|
||||||
if (AudioService.sink) {
|
|
||||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExpandClicked: root.toggleSection("audio_output")
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioInputPill {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
expanded: root.expandedSection === "audio_input"
|
|
||||||
onClicked: {
|
|
||||||
if (AudioService.source) {
|
|
||||||
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExpandClicked: root.toggleSection("audio_input")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
active: root.expandedSection === "audio_output" || root.expandedSection === "audio_input"
|
|
||||||
visible: active
|
|
||||||
sourceComponent: DetailView {
|
|
||||||
width: parent.width
|
|
||||||
isVisible: true
|
|
||||||
title: {
|
|
||||||
switch (root.expandedSection) {
|
|
||||||
case "audio_output": return "Audio Output"
|
|
||||||
case "audio_input": return "Audio Input"
|
|
||||||
default: return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content: {
|
|
||||||
switch (root.expandedSection) {
|
|
||||||
case "audio_output": return audioOutputDetailComponent
|
|
||||||
case "audio_input": return audioInputDetailComponent
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contentHeight: 250
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
ToggleButton {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
iconName: DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
|
|
||||||
text: "Night Mode"
|
|
||||||
secondaryText: SessionData.nightModeAutoEnabled ? "Auto" : (DisplayService.nightModeEnabled ? "On" : "Off")
|
|
||||||
isActive: DisplayService.nightModeEnabled
|
|
||||||
enabled: DisplayService.automationAvailable
|
|
||||||
onClicked: DisplayService.toggleNightMode()
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
name: "schedule"
|
|
||||||
size: 12
|
|
||||||
color: Theme.primary
|
|
||||||
visible: SessionData.nightModeAutoEnabled
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleButton {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
iconName: SessionData.isLightMode ? "light_mode" : "palette"
|
|
||||||
text: "Theme"
|
|
||||||
secondaryText: SessionData.isLightMode ? "Light" : "Dark"
|
|
||||||
isActive: true
|
|
||||||
onClicked: Theme.toggleLightMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Details.BluetoothCodecSelector {
|
|
||||||
id: bluetoothCodecSelector
|
|
||||||
anchors.fill: parent
|
|
||||||
z: 10000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: networkDetailComponent
|
|
||||||
NetworkDetail {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: bluetoothDetailComponent
|
|
||||||
BluetoothDetail {
|
|
||||||
id: bluetoothDetail
|
|
||||||
onShowCodecSelector: function(device) {
|
|
||||||
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
|
|
||||||
contentLoader.item.bluetoothCodecSelector.show(device)
|
|
||||||
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
|
|
||||||
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: audioOutputDetailComponent
|
|
||||||
AudioOutputDetail {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: audioInputDetailComponent
|
|
||||||
AudioInputDetail {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
implicitHeight: headerRow.height + volumeSlider.height + audioContent.height + Theme.spacingM
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
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: "Input Devices"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
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
|
|
||||||
value: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
leftIcon: {
|
|
||||||
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
|
|
||||||
let muted = AudioService.source.audio.muted
|
|
||||||
return muted ? "mic_off" : "mic"
|
|
||||||
}
|
|
||||||
rightIcon: "volume_up"
|
|
||||||
enabled: AudioService.source && AudioService.source.audio
|
|
||||||
unit: "%"
|
|
||||||
showValue: true
|
|
||||||
visible: AudioService.source && AudioService.source.audio
|
|
||||||
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (AudioService.source && AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.volume = newValue / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
height: parent.height
|
|
||||||
onClicked: {
|
|
||||||
if (AudioService.source && AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
id: audioContent
|
|
||||||
anchors.top: volumeSlider.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
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) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
|
||||||
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
border.width: modelData === AudioService.source ? 2 : 1
|
|
||||||
|
|
||||||
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("usb"))
|
|
||||||
return "headset"
|
|
||||||
else
|
|
||||||
return "mic"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: modelData === AudioService.source ? 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.source ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData === AudioService.source ? "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.preferredDefaultAudioSource = modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
implicitHeight: headerRow.height + audioContent.height + Theme.spacingM
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
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: "Audio Devices"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
id: audioContent
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.topMargin: 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) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
border.width: modelData === AudioService.sink ? 2 : 1
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,533 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modals
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
implicitHeight: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return headerRow.height + wifiToggleContent.height + Theme.spacingM
|
|
||||||
}
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
return headerRow.height + wifiContent.height + Theme.spacingM
|
|
||||||
}
|
|
||||||
return headerRow.height + wifiOffContent.height + Theme.spacingM
|
|
||||||
}
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
NetworkService.addRef()
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
NetworkService.removeRef()
|
|
||||||
}
|
|
||||||
|
|
||||||
property var wifiPasswordModalRef: {
|
|
||||||
wifiPasswordModalLoader.active = true
|
|
||||||
return wifiPasswordModalLoader.item
|
|
||||||
}
|
|
||||||
property var networkInfoModalRef: {
|
|
||||||
networkInfoModalLoader.active = true
|
|
||||||
return networkInfoModalLoader.item
|
|
||||||
}
|
|
||||||
|
|
||||||
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: "Network Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM)
|
|
||||||
height: parent.height
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: preferenceControls
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool isActive: NetworkService.userPreference === "ethernet"
|
|
||||||
|
|
||||||
width: 90
|
|
||||||
height: 36
|
|
||||||
radius: 18
|
|
||||||
color: isActive ? Theme.surfaceContainerHigh : ethernetMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Ethernet"
|
|
||||||
color: parent.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: parent.isActive ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: ethernetMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: NetworkService.setNetworkPreference("ethernet")
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool isActive: NetworkService.userPreference === "wifi"
|
|
||||||
|
|
||||||
width: 70
|
|
||||||
height: 36
|
|
||||||
radius: 18
|
|
||||||
color: isActive ? Theme.surfaceContainerHigh : wifiMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "WiFi"
|
|
||||||
color: parent.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: parent.isActive ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: wifiMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: NetworkService.setNetworkPreference("wifi")
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: wifiToggleContent
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
visible: NetworkService.wifiToggling
|
|
||||||
height: visible ? 80 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
name: "sync"
|
|
||||||
size: 32
|
|
||||||
color: Theme.primary
|
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
|
||||||
running: NetworkService.wifiToggling
|
|
||||||
loops: Animation.Infinite
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: wifiOffContent
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
|
||||||
height: visible ? 120 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
name: "wifi_off"
|
|
||||||
size: 48
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: "WiFi is off"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
width: 120
|
|
||||||
height: 36
|
|
||||||
radius: 18
|
|
||||||
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primary
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Enable WiFi"
|
|
||||||
color: Theme.primary
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: enableWifiButton
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: NetworkService.toggleWifiRadio()
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
id: wifiContent
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
visible: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
|
||||||
contentHeight: wifiColumn.height
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: wifiColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 200
|
|
||||||
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "refresh"
|
|
||||||
size: 48
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
|
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
|
||||||
running: true
|
|
||||||
loops: Animation.Infinite
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
let networks = [...NetworkService.wifiNetworks]
|
|
||||||
networks.sort((a, b) => {
|
|
||||||
if (a.ssid === NetworkService.currentWifiSSID) return -1
|
|
||||||
if (b.ssid === NetworkService.currentWifiSSID) return 1
|
|
||||||
return b.signal - a.signal
|
|
||||||
})
|
|
||||||
return networks
|
|
||||||
}
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: networkMouseArea.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, index % 2 === 0 ? 0.3 : 0.2)
|
|
||||||
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
border.width: modelData.ssid === NetworkService.currentWifiSSID ? 2 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
let strength = modelData.signal || 0
|
|
||||||
if (strength >= 70) return "signal_wifi_4_bar"
|
|
||||||
if (strength >= 50) return "network_wifi_3_bar"
|
|
||||||
if (strength >= 25) return "network_wifi_2_bar"
|
|
||||||
if (strength >= 10) return "network_wifi_1_bar"
|
|
||||||
return "signal_wifi_bad"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 200
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.ssid || "Unknown Network"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.saved ? "• Saved" : ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.primary
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• " + modelData.signal + "%"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: optionsButton
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
iconName: "more_horiz"
|
|
||||||
buttonSize: 28
|
|
||||||
onClicked: {
|
|
||||||
if (networkContextMenu.visible) {
|
|
||||||
networkContextMenu.close()
|
|
||||||
} else {
|
|
||||||
networkContextMenu.currentSSID = modelData.ssid
|
|
||||||
networkContextMenu.currentSecured = modelData.secured
|
|
||||||
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID
|
|
||||||
networkContextMenu.currentSaved = modelData.saved
|
|
||||||
networkContextMenu.currentSignal = modelData.signal
|
|
||||||
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: networkMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: optionsButton.width + Theme.spacingS
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: function(event) {
|
|
||||||
if (modelData.ssid !== NetworkService.currentWifiSSID) {
|
|
||||||
if (modelData.secured && !modelData.saved) {
|
|
||||||
if (wifiPasswordModalRef) {
|
|
||||||
wifiPasswordModalRef.show(modelData.ssid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NetworkService.connectToWifi(modelData.ssid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: networkContextMenu
|
|
||||||
width: 150
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
|
||||||
|
|
||||||
property string currentSSID: ""
|
|
||||||
property bool currentSecured: false
|
|
||||||
property bool currentConnected: false
|
|
||||||
property bool currentSaved: false
|
|
||||||
property int currentSignal: 0
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: networkContextMenu.currentConnected ? "Disconnect" : "Connect"
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
contentItem: StyledText {
|
|
||||||
text: parent.text
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
leftPadding: Theme.spacingS
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
|
||||||
radius: Theme.cornerRadius / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
if (networkContextMenu.currentConnected) {
|
|
||||||
NetworkService.disconnectWifi()
|
|
||||||
} else {
|
|
||||||
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
|
||||||
if (wifiPasswordModalRef) {
|
|
||||||
wifiPasswordModalRef.show(networkContextMenu.currentSSID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NetworkService.connectToWifi(networkContextMenu.currentSSID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: "Network Info"
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
contentItem: StyledText {
|
|
||||||
text: parent.text
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
leftPadding: Theme.spacingS
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
|
||||||
radius: Theme.cornerRadius / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
if (networkInfoModalRef) {
|
|
||||||
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
|
|
||||||
networkInfoModalRef.showNetworkInfo(networkContextMenu.currentSSID, networkData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: "Forget Network"
|
|
||||||
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
|
|
||||||
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
|
|
||||||
|
|
||||||
contentItem: StyledText {
|
|
||||||
text: parent.text
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.error
|
|
||||||
leftPadding: Theme.spacingS
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
|
||||||
radius: Theme.cornerRadius / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: wifiPasswordModalLoader
|
|
||||||
active: false
|
|
||||||
|
|
||||||
WifiPasswordModal {
|
|
||||||
id: wifiPasswordModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: networkInfoModalLoader
|
|
||||||
active: false
|
|
||||||
|
|
||||||
NetworkInfoModal {
|
|
||||||
id: networkInfoModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,312 +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
|
|
||||||
|
|
||||||
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: 1
|
|
||||||
opacity: powerMenuVisible ? 1 : 0
|
|
||||||
scale: powerMenuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Power Options"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g,
|
|
||||||
Theme.error.b, 0.12)
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Log Out"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.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: "Suspend"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.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: "Reboot"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.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: "Power Off"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: powerOffArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.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,57 +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
|
|
||||||
|
|
||||||
BasePill {
|
|
||||||
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) + "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
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,60 +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
|
|
||||||
|
|
||||||
BasePill {
|
|
||||||
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) + "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
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,50 +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
|
|
||||||
|
|
||||||
SimpleSlider {
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
iconColor: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
|
||||||
|
|
||||||
enabled: defaultSink !== null
|
|
||||||
allowIconClick: defaultSink !== null
|
|
||||||
|
|
||||||
value: defaultSink ? defaultSink.audio.volume : 0.0
|
|
||||||
maximumValue: 1.0
|
|
||||||
minimumValue: 0.0
|
|
||||||
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (defaultSink) {
|
|
||||||
defaultSink.audio.volume = newValue
|
|
||||||
if (newValue > 0 && defaultSink.audio.muted) {
|
|
||||||
defaultSink.audio.muted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onIconClicked: function() {
|
|
||||||
if (defaultSink) {
|
|
||||||
defaultSink.audio.muted = !defaultSink.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var defaultSink: AudioService.sink
|
|
||||||
|
|
||||||
height: 40
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
height: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
radius: (Theme.iconSize + Theme.spacingS * 2) / 2 // Make it circular
|
|
||||||
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: iconArea
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: defaultSink !== null
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (defaultSink) {
|
|
||||||
defaultSink.audio.muted = !defaultSink.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM
|
|
||||||
enabled: defaultSink !== null
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (defaultSink) {
|
|
||||||
defaultSink.audio.volume = newValue / 100.0
|
|
||||||
if (newValue > 0 && defaultSink.audio.muted) {
|
|
||||||
defaultSink.audio.muted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string iconName: ""
|
|
||||||
property color iconColor: Theme.surfaceText
|
|
||||||
property string primaryText: ""
|
|
||||||
property string secondaryText: ""
|
|
||||||
property bool expanded: false
|
|
||||||
property bool isActive: false
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
signal expandClicked()
|
|
||||||
signal wheelEvent(var wheelEvent)
|
|
||||||
|
|
||||||
width: parent ? parent.width : 200
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: mainArea
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: parent.width - expandArea.width
|
|
||||||
topLeftRadius: Theme.cornerRadius
|
|
||||||
bottomLeftRadius: Theme.cornerRadius
|
|
||||||
topRightRadius: 0
|
|
||||||
bottomRightRadius: 0
|
|
||||||
color: mainAreaMouse.containsMouse ?
|
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
|
||||||
"transparent"
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.iconName
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: root.isActive ? Theme.primary : root.iconColor
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingS
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: root.primaryText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: root.secondaryText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: text.length > 0
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mainAreaMouse
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.clicked()
|
|
||||||
onWheel: function (wheelEvent) {
|
|
||||||
root.wheelEvent(wheelEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: expandArea
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: Theme.iconSize + Theme.spacingM * 2
|
|
||||||
topLeftRadius: 0
|
|
||||||
bottomLeftRadius: 0
|
|
||||||
topRightRadius: Theme.cornerRadius
|
|
||||||
bottomRightRadius: Theme.cornerRadius
|
|
||||||
color: expandAreaMouse.containsMouse ?
|
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
|
||||||
"transparent"
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: expandIcon
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: expanded ? "expand_less" : "expand_more"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: expandAreaMouse
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.expandClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Widgets
|
|
||||||
|
|
||||||
BasePill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var primaryDevice: {
|
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
|
||||||
for (let device of devices) {
|
|
||||||
if (device && device.connected) {
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
iconName: {
|
|
||||||
if (!BluetoothService.available) {
|
|
||||||
return "bluetooth_disabled"
|
|
||||||
}
|
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
|
||||||
return "bluetooth_disabled"
|
|
||||||
}
|
|
||||||
if (primaryDevice) {
|
|
||||||
return BluetoothService.getDeviceIcon(primaryDevice)
|
|
||||||
}
|
|
||||||
return "bluetooth"
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
|
|
||||||
|
|
||||||
primaryText: {
|
|
||||||
if (!BluetoothService.available) {
|
|
||||||
return "Bluetooth unavailable"
|
|
||||||
}
|
|
||||||
if (!BluetoothService.adapter) {
|
|
||||||
return "No adapter"
|
|
||||||
}
|
|
||||||
if (!BluetoothService.adapter.enabled) {
|
|
||||||
return "Disabled"
|
|
||||||
}
|
|
||||||
return "Enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
secondaryText: {
|
|
||||||
if (!BluetoothService.available) {
|
|
||||||
return "Hardware not found"
|
|
||||||
}
|
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
|
||||||
return "Off"
|
|
||||||
}
|
|
||||||
if (primaryDevice) {
|
|
||||||
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
|
|
||||||
}
|
|
||||||
return "No devices"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Widgets
|
|
||||||
|
|
||||||
SimpleSlider {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
iconName: {
|
|
||||||
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
|
||||||
|
|
||||||
let brightness = DisplayService.brightnessLevel
|
|
||||||
if (brightness <= 33) return "brightness_low"
|
|
||||||
if (brightness <= 66) return "brightness_medium"
|
|
||||||
return "brightness_high"
|
|
||||||
}
|
|
||||||
|
|
||||||
iconColor: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
|
||||||
|
|
||||||
enabled: DisplayService.brightnessAvailable
|
|
||||||
|
|
||||||
value: DisplayService.brightnessLevel
|
|
||||||
maximumValue: 100.0
|
|
||||||
minimumValue: 0.0
|
|
||||||
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
DisplayService.brightnessLevel = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
height: 40
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
height: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
|
||||||
color: iconArea.containsMouse
|
|
||||||
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
: "transparent"
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: iconArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: function(event) {
|
|
||||||
if (DisplayService.devices.length > 1) {
|
|
||||||
if (deviceMenu.visible) {
|
|
||||||
deviceMenu.close()
|
|
||||||
} else {
|
|
||||||
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
|
||||||
|
|
||||||
let brightness = DisplayService.brightnessLevel
|
|
||||||
if (brightness <= 33) return "brightness_low"
|
|
||||||
if (brightness <= 66) return "brightness_medium"
|
|
||||||
return "brightness_high"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM
|
|
||||||
enabled: DisplayService.brightnessAvailable
|
|
||||||
minimum: 1
|
|
||||||
maximum: 100
|
|
||||||
value: {
|
|
||||||
let level = DisplayService.brightnessLevel
|
|
||||||
if (level > 100) {
|
|
||||||
let deviceInfo = DisplayService.getCurrentDeviceInfo()
|
|
||||||
if (deviceInfo && deviceInfo.max > 0) {
|
|
||||||
return Math.round((level / deviceInfo.max) * 100)
|
|
||||||
}
|
|
||||||
return 50
|
|
||||||
}
|
|
||||||
return level
|
|
||||||
}
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
DisplayService.setBrightness(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: deviceMenu
|
|
||||||
width: 200
|
|
||||||
closePolicy: Popup.CloseOnEscape
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
}
|
|
||||||
|
|
||||||
Instantiator {
|
|
||||||
model: DisplayService.devices
|
|
||||||
delegate: MenuItem {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
property string deviceName: modelData.name || ""
|
|
||||||
property string deviceClass: modelData.class || ""
|
|
||||||
|
|
||||||
text: deviceName
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
indicator: Rectangle {
|
|
||||||
visible: DisplayService.currentDevice === parent.deviceName
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
width: 4
|
|
||||||
height: parent.height - Theme.spacingS * 2
|
|
||||||
radius: 2
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: StyledText {
|
|
||||||
text: parent.text
|
|
||||||
font: parent.font
|
|
||||||
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
|
|
||||||
leftPadding: Theme.spacingL
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
|
||||||
radius: Theme.cornerRadius / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
DisplayService.setCurrentDevice(deviceName, true)
|
|
||||||
deviceMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
|
|
||||||
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Widgets
|
|
||||||
|
|
||||||
BasePill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
isActive: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return NetworkService.wifiEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
iconName: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return "sync"
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "settings_ethernet"
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi") {
|
|
||||||
return NetworkService.wifiSignalIcon
|
|
||||||
}
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
return "signal_wifi_off"
|
|
||||||
}
|
|
||||||
return "wifi_off"
|
|
||||||
}
|
|
||||||
|
|
||||||
primaryText: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "Ethernet"
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
|
|
||||||
return NetworkService.currentWifiSSID
|
|
||||||
}
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
return "Not connected"
|
|
||||||
}
|
|
||||||
return "WiFi off"
|
|
||||||
}
|
|
||||||
|
|
||||||
secondaryText: {
|
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return "Please wait..."
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
|
||||||
return "Connected"
|
|
||||||
}
|
|
||||||
if (NetworkService.networkStatus === "wifi") {
|
|
||||||
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
|
|
||||||
}
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
return "Select network"
|
|
||||||
}
|
|
||||||
return "Tap to enable"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string iconName: ""
|
|
||||||
property color iconColor: Theme.surfaceText
|
|
||||||
property real value: 0.0
|
|
||||||
property real maximumValue: 1.0
|
|
||||||
property real minimumValue: 0.0
|
|
||||||
property bool enabled: true
|
|
||||||
property bool allowIconClick: false
|
|
||||||
|
|
||||||
signal sliderValueChanged(real value)
|
|
||||||
signal iconClicked()
|
|
||||||
|
|
||||||
height: 60
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.iconName
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: root.iconColor
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.allowIconClick
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.iconClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: {
|
|
||||||
if (parent.width <= 0) return 80
|
|
||||||
return Math.max(80, Math.min(400, parent.width - Theme.iconSize - Theme.spacingM))
|
|
||||||
}
|
|
||||||
enabled: root.enabled
|
|
||||||
minimum: Math.round(root.minimumValue * 100)
|
|
||||||
maximum: Math.round(root.maximumValue * 100)
|
|
||||||
value: Math.round(root.value * 100)
|
|
||||||
onSliderValueChanged: function(newValue) { root.sliderValueChanged(newValue / 100.0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string iconName: ""
|
|
||||||
property string text: ""
|
|
||||||
property bool isActive: false
|
|
||||||
property bool enabled: true
|
|
||||||
property string secondaryText: ""
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
width: parent ? parent.width : 200
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
opacity: enabled ? 1.0 : 0.6
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: mouseArea.containsMouse ?
|
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
|
||||||
"transparent"
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation { duration: Theme.shortDuration }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.iconName
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingS
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: root.text
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: root.secondaryText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: text.length > 0
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: root.enabled
|
|
||||||
onClicked: root.clicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: dock
|
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
property var modelData
|
|
||||||
property var contextMenu
|
|
||||||
property bool autoHide: SettingsData.dockAutoHide
|
|
||||||
property real backgroundTransparency: SettingsData.dockTransparency
|
|
||||||
|
|
||||||
property bool contextMenuOpen: (contextMenu && contextMenu.visible
|
|
||||||
&& contextMenu.screen === modelData)
|
|
||||||
property bool windowIsFullscreen: {
|
|
||||||
if (!ToplevelManager.activeToplevel)
|
|
||||||
return false
|
|
||||||
var activeWindow = ToplevelManager.activeToplevel
|
|
||||||
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
|
|
||||||
return fullscreenApps.some(app => activeWindow.appId
|
|
||||||
&& activeWindow.appId.toLowerCase(
|
|
||||||
).includes(app))
|
|
||||||
}
|
|
||||||
property bool reveal: (!autoHide || dockMouseArea.containsMouse
|
|
||||||
|| dockApps.requestDockShow || contextMenuOpen)
|
|
||||||
&& !windowIsFullscreen
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onDockTransparencyChanged() {
|
|
||||||
dock.backgroundTransparency = SettingsData.dockTransparency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: modelData
|
|
||||||
visible: SettingsData.showDock
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
bottom: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
}
|
|
||||||
|
|
||||||
margins {
|
|
||||||
left: 0
|
|
||||||
right: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: 100
|
|
||||||
exclusiveZone: autoHide ? -1 : 65 - 16
|
|
||||||
|
|
||||||
mask: Region {
|
|
||||||
item: dockMouseArea
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dockMouseArea
|
|
||||||
property real currentScreen: modelData ? modelData : dock.screen
|
|
||||||
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
|
|
||||||
property real maxDockWidth: Math.min(screenWidth * 0.8, 1200)
|
|
||||||
|
|
||||||
height: dock.reveal ? 65 : 20
|
|
||||||
width: dock.reveal ? Math.min(dockBackground.width + 32, maxDockWidth) : Math.min(Math.max(dockBackground.width + 64, 200), screenWidth * 0.5)
|
|
||||||
anchors {
|
|
||||||
bottom: parent.bottom
|
|
||||||
horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: dockContainer
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
id: dockSlide
|
|
||||||
y: dock.reveal ? 0 : 60
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: dockBackground
|
|
||||||
objectName: "dockBackground"
|
|
||||||
anchors {
|
|
||||||
top: parent.top
|
|
||||||
bottom: parent.bottom
|
|
||||||
horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
width: dockApps.implicitWidth + 12
|
|
||||||
height: parent.height - 8
|
|
||||||
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.bottomMargin: 1
|
|
||||||
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
|
||||||
Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, backgroundTransparency)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
|
|
||||||
Theme.surfaceTint.b, 0.04)
|
|
||||||
radius: parent.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
DockApps {
|
|
||||||
id: dockApps
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.bottomMargin: 4
|
|
||||||
|
|
||||||
contextMenu: dock.contextMenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: appTooltip
|
|
||||||
|
|
||||||
property var hoveredButton: {
|
|
||||||
if (!dockApps.children[0])
|
|
||||||
return null
|
|
||||||
var row = dockApps.children[0]
|
|
||||||
var repeater = null
|
|
||||||
for (var i = 0; i < row.children.length; i++) {
|
|
||||||
var child = row.children[i]
|
|
||||||
if (child && typeof child.count !== "undefined"
|
|
||||||
&& typeof child.itemAt === "function") {
|
|
||||||
repeater = child
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!repeater || !repeater.itemAt)
|
|
||||||
return null
|
|
||||||
for (var i = 0; i < repeater.count; i++) {
|
|
||||||
var item = repeater.itemAt(i)
|
|
||||||
if (item && item.dockButton
|
|
||||||
&& item.dockButton.showTooltip) {
|
|
||||||
return item.dockButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
|
|
||||||
|
|
||||||
visible: hoveredButton !== null && tooltipText !== ""
|
|
||||||
width: tooltipLabel.implicitWidth + 24
|
|
||||||
height: tooltipLabel.implicitHeight + 12
|
|
||||||
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
|
|
||||||
y: -height - 8
|
|
||||||
x: hoveredButton ? hoveredButton.mapToItem(
|
|
||||||
dockContainer, hoveredButton.width / 2,
|
|
||||||
0).x - width / 2 : 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: appTooltip.tooltipText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
clip: false
|
|
||||||
property var appData
|
|
||||||
property var contextMenu: null
|
|
||||||
property var dockApps: null
|
|
||||||
property int index: -1
|
|
||||||
property bool longPressing: false
|
|
||||||
property bool dragging: false
|
|
||||||
property point dragStartPos: Qt.point(0, 0)
|
|
||||||
property point dragOffset: Qt.point(0, 0)
|
|
||||||
property int targetIndex: -1
|
|
||||||
property int originalIndex: -1
|
|
||||||
property bool showWindowTitle: false
|
|
||||||
property string windowTitle: ""
|
|
||||||
property bool isHovered: mouseArea.containsMouse && !dragging
|
|
||||||
property bool showTooltip: mouseArea.containsMouse && !dragging
|
|
||||||
property bool isWindowFocused: {
|
|
||||||
if (!appData || appData.type !== "window") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var toplevel = getToplevelObject()
|
|
||||||
if (!toplevel) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return toplevel.activated
|
|
||||||
}
|
|
||||||
property string tooltipText: {
|
|
||||||
if (!appData)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
// For window type, show app name + window title
|
|
||||||
if (appData.type === "window" && showWindowTitle) {
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
var appName = desktopEntry
|
|
||||||
&& desktopEntry.name ? desktopEntry.name : appData.appId
|
|
||||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
|
||||||
}
|
|
||||||
// For pinned apps, just show app name
|
|
||||||
if (!appData.appId)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
return desktopEntry
|
|
||||||
&& desktopEntry.name ? desktopEntry.name : appData.appId
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
function getToplevelObject() {
|
|
||||||
if (!appData || appData.type !== "window") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortedToplevels = CompositorService.sortedToplevels
|
|
||||||
if (!sortedToplevels) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.uniqueId) {
|
|
||||||
for (var i = 0; i < sortedToplevels.length; i++) {
|
|
||||||
var toplevel = sortedToplevels[i]
|
|
||||||
var 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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
onIsHoveredChanged: {
|
|
||||||
if (isHovered) {
|
|
||||||
exitAnimation.stop()
|
|
||||||
if (!bounceAnimation.running)
|
|
||||||
bounceAnimation.restart()
|
|
||||||
} else {
|
|
||||||
bounceAnimation.stop()
|
|
||||||
exitAnimation.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: bounceAnimation
|
|
||||||
|
|
||||||
running: false
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: translateY
|
|
||||||
property: "y"
|
|
||||||
to: -10
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedAccel
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: translateY
|
|
||||||
property: "y"
|
|
||||||
to: -8
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedDecel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: exitAnimation
|
|
||||||
|
|
||||||
running: false
|
|
||||||
target: translateY
|
|
||||||
property: "y"
|
|
||||||
to: 0
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasizedDecel
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
|
||||||
border.width: 2
|
|
||||||
border.color: Theme.primary
|
|
||||||
visible: dragging
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: longPressTimer
|
|
||||||
|
|
||||||
interval: 500
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (appData && appData.isPinned)
|
|
||||||
longPressing = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.bottomMargin: -20
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
onPressed: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton && appData
|
|
||||||
&& appData.isPinned) {
|
|
||||||
dragStartPos = Qt.point(mouse.x, mouse.y)
|
|
||||||
longPressTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: mouse => {
|
|
||||||
longPressTimer.stop()
|
|
||||||
if (longPressing) {
|
|
||||||
if (dragging && targetIndex >= 0
|
|
||||||
&& targetIndex !== originalIndex && dockApps)
|
|
||||||
dockApps.movePinnedApp(originalIndex, targetIndex)
|
|
||||||
|
|
||||||
longPressing = false
|
|
||||||
dragging = false
|
|
||||||
dragOffset = Qt.point(0, 0)
|
|
||||||
targetIndex = -1
|
|
||||||
originalIndex = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (longPressing && !dragging) {
|
|
||||||
var distance = Math.sqrt(
|
|
||||||
Math.pow(mouse.x - dragStartPos.x,
|
|
||||||
2) + Math.pow(
|
|
||||||
mouse.y - dragStartPos.y, 2))
|
|
||||||
if (distance > 5) {
|
|
||||||
dragging = true
|
|
||||||
targetIndex = index
|
|
||||||
originalIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dragging) {
|
|
||||||
dragOffset = Qt.point(
|
|
||||||
mouse.x - dragStartPos.x,
|
|
||||||
mouse.y - dragStartPos.y)
|
|
||||||
if (dockApps) {
|
|
||||||
var threshold = 40
|
|
||||||
var newTargetIndex = targetIndex
|
|
||||||
if (dragOffset.x > threshold
|
|
||||||
&& targetIndex < dockApps.pinnedAppCount - 1)
|
|
||||||
newTargetIndex = targetIndex + 1
|
|
||||||
else if (dragOffset.x < -threshold
|
|
||||||
&& targetIndex > 0)
|
|
||||||
newTargetIndex = targetIndex - 1
|
|
||||||
if (newTargetIndex !== targetIndex) {
|
|
||||||
targetIndex = newTargetIndex
|
|
||||||
dragStartPos = Qt.point(mouse.x,
|
|
||||||
mouse.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (!appData || longPressing)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
// Handle based on type
|
|
||||||
if (appData.type === "pinned") {
|
|
||||||
// Launch the pinned app
|
|
||||||
if (appData && appData.appId) {
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(
|
|
||||||
appData.appId)
|
|
||||||
if (desktopEntry)
|
|
||||||
AppUsageHistoryData.addAppUsage({
|
|
||||||
"id": appData.appId,
|
|
||||||
"name": desktopEntry.name
|
|
||||||
|| appData.appId,
|
|
||||||
"icon": desktopEntry.icon
|
|
||||||
|| "",
|
|
||||||
"exec": desktopEntry.exec
|
|
||||||
|| "",
|
|
||||||
"comment": desktopEntry.comment || ""
|
|
||||||
})
|
|
||||||
|
|
||||||
desktopEntry.execute()
|
|
||||||
}
|
|
||||||
} else if (appData.type === "window") {
|
|
||||||
var toplevel = getToplevelObject()
|
|
||||||
if (toplevel) {
|
|
||||||
toplevel.activate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.MiddleButton) {
|
|
||||||
if (appData && appData.appId) {
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(
|
|
||||||
appData.appId)
|
|
||||||
if (desktopEntry)
|
|
||||||
AppUsageHistoryData.addAppUsage({
|
|
||||||
"id": appData.appId,
|
|
||||||
"name": desktopEntry.name
|
|
||||||
|| appData.appId,
|
|
||||||
"icon": desktopEntry.icon
|
|
||||||
|| "",
|
|
||||||
"exec": desktopEntry.exec
|
|
||||||
|| "",
|
|
||||||
"comment": desktopEntry.comment
|
|
||||||
|| ""
|
|
||||||
})
|
|
||||||
|
|
||||||
desktopEntry.execute()
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
if (contextMenu)
|
|
||||||
contextMenu.showForButton(root, appData, 40)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
implicitSize: 40
|
|
||||||
source: {
|
|
||||||
if (appData.appId === "__SEPARATOR__") return ""
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(Paths.moddedAppId(appData.appId))
|
|
||||||
return desktopEntry && desktopEntry.icon ? Quickshell.iconPath(desktopEntry.icon, true) : ""
|
|
||||||
}
|
|
||||||
mipmap: true
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: iconImg.status !== Image.Ready
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
if (!appData || !appData.appId)
|
|
||||||
return "?"
|
|
||||||
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
|
|
||||||
if (desktopEntry && desktopEntry.name)
|
|
||||||
return desktopEntry.name.charAt(0).toUpperCase()
|
|
||||||
|
|
||||||
return appData.appId.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indicator for running/focused state
|
|
||||||
Rectangle {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: -2
|
|
||||||
width: 8
|
|
||||||
height: 2
|
|
||||||
radius: 1
|
|
||||||
visible: appData && (appData.isRunning || appData.type === "window")
|
|
||||||
color: {
|
|
||||||
if (!appData)
|
|
||||||
return "transparent"
|
|
||||||
|
|
||||||
// For window type, check if focused using reactive property
|
|
||||||
if (isWindowFocused)
|
|
||||||
return Theme.primary
|
|
||||||
|
|
||||||
// For running apps, show dimmer indicator
|
|
||||||
if (appData.isRunning || appData.type === "window")
|
|
||||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
id: translateY
|
|
||||||
|
|
||||||
y: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +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
|
|
||||||
|
|
||||||
implicitWidth: row.width
|
|
||||||
implicitHeight: row.height
|
|
||||||
|
|
||||||
function movePinnedApp(fromIndex, toIndex) {
|
|
||||||
if (fromIndex === toIndex)
|
|
||||||
return
|
|
||||||
|
|
||||||
var currentPinned = [...(SessionData.pinnedApps || [])]
|
|
||||||
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0
|
|
||||||
|| toIndex >= currentPinned.length)
|
|
||||||
return
|
|
||||||
|
|
||||||
var movedApp = currentPinned.splice(fromIndex, 1)[0]
|
|
||||||
currentPinned.splice(toIndex, 0, movedApp)
|
|
||||||
|
|
||||||
SessionData.setPinnedApps(currentPinned)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: row
|
|
||||||
spacing: 2
|
|
||||||
anchors.centerIn: parent
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: repeater
|
|
||||||
model: ListModel {
|
|
||||||
id: dockModel
|
|
||||||
|
|
||||||
Component.onCompleted: updateModel()
|
|
||||||
|
|
||||||
function updateModel() {
|
|
||||||
clear()
|
|
||||||
|
|
||||||
var items = []
|
|
||||||
var pinnedApps = [...(SessionData.pinnedApps || [])]
|
|
||||||
|
|
||||||
// First section: Pinned apps (always visible, not representing running windows)
|
|
||||||
pinnedApps.forEach(appId => {
|
|
||||||
items.push({
|
|
||||||
"type": "pinned",
|
|
||||||
"appId": appId,
|
|
||||||
"windowId": -1,
|
|
||||||
"windowTitle"// Use -1 instead of null to avoid ListModel warnings
|
|
||||||
: "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned"// Use -1 instead of null
|
|
||||||
: true,
|
|
||||||
"isRunning": false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
root.pinnedAppCount = pinnedApps.length
|
|
||||||
|
|
||||||
// Get sorted toplevels from CompositorService
|
|
||||||
var sortedToplevels = CompositorService.sortedToplevels
|
|
||||||
|
|
||||||
// Add separator between pinned and running if both exist
|
|
||||||
if (pinnedApps.length > 0
|
|
||||||
&& sortedToplevels.length > 0) {
|
|
||||||
items.push({
|
|
||||||
"type": "separator",
|
|
||||||
"appId": "__SEPARATOR__",
|
|
||||||
"windowId": -1,
|
|
||||||
"windowTitle"// Use -1 instead of null
|
|
||||||
: "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned"// Use -1 instead of null
|
|
||||||
: false,
|
|
||||||
"isRunning": false,
|
|
||||||
"isFocused": false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second section: Running windows (sorted using Theme.sortToplevels)
|
|
||||||
sortedToplevels.forEach((toplevel, index) => {
|
|
||||||
// Limit window title length for tooltip
|
|
||||||
var title = toplevel.title || "(Unnamed)"
|
|
||||||
if (title.length > 50) {
|
|
||||||
title = title.substring(0, 47) + "..."
|
|
||||||
}
|
|
||||||
var uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
"type": "window",
|
|
||||||
"appId": toplevel.appId,
|
|
||||||
"windowId": index,
|
|
||||||
"windowTitle": title,
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": false,
|
|
||||||
"isRunning": true,
|
|
||||||
"uniqueId": uniqueId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
append(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateItem
|
|
||||||
property alias dockButton: button
|
|
||||||
|
|
||||||
width: model.type === "separator" ? 16 : 40
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: model.type === "separator"
|
|
||||||
width: 2
|
|
||||||
height: 20
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
radius: 1
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
DockAppButton {
|
|
||||||
id: button
|
|
||||||
visible: model.type !== "separator"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
appData: model
|
|
||||||
contextMenu: root.contextMenu
|
|
||||||
dockApps: root
|
|
||||||
index: model.index
|
|
||||||
|
|
||||||
// Override tooltip for windows to show window title
|
|
||||||
showWindowTitle: model.type === "window"
|
|
||||||
windowTitle: model.windowTitle || ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: CompositorService
|
|
||||||
function onSortedToplevelsChanged() {
|
|
||||||
dockModel.updateModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onPinnedAppsChanged() {
|
|
||||||
dockModel.updateModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showContextMenu: false
|
|
||||||
property var appData: null
|
|
||||||
property var anchorItem: null
|
|
||||||
property real dockVisibleHeight: 40
|
|
||||||
property int margin: 10
|
|
||||||
|
|
||||||
function showForButton(button, data, dockHeight) {
|
|
||||||
anchorItem = button
|
|
||||||
appData = data
|
|
||||||
dockVisibleHeight = dockHeight || 40
|
|
||||||
|
|
||||||
var dockWindow = button.Window.window
|
|
||||||
if (dockWindow) {
|
|
||||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
|
||||||
var s = Quickshell.screens[i]
|
|
||||||
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
|
|
||||||
root.screen = s
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showContextMenu = true
|
|
||||||
}
|
|
||||||
function close() {
|
|
||||||
showContextMenu = false
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: Quickshell.screens[0]
|
|
||||||
|
|
||||||
visible: showContextMenu
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
|
|
||||||
|
|
||||||
onAnchorItemChanged: updatePosition()
|
|
||||||
onVisibleChanged: if (visible)
|
|
||||||
updatePosition()
|
|
||||||
|
|
||||||
function updatePosition() {
|
|
||||||
if (!anchorItem) {
|
|
||||||
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var dockWindow = anchorItem.Window.window
|
|
||||||
if (!dockWindow) {
|
|
||||||
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
|
|
||||||
|
|
||||||
var actualDockHeight = root.dockVisibleHeight // fallback
|
|
||||||
|
|
||||||
function findDockBackground(item) {
|
|
||||||
if (item.objectName === "dockBackground") {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
for (var i = 0; i < item.children.length; i++) {
|
|
||||||
var found = findDockBackground(item.children[i])
|
|
||||||
if (found)
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
var dockBackground = findDockBackground(dockWindow.contentItem)
|
|
||||||
if (dockBackground) {
|
|
||||||
actualDockHeight = dockBackground.height
|
|
||||||
}
|
|
||||||
|
|
||||||
var dockBottomMargin = 16 // The dock has bottom margin
|
|
||||||
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
|
|
||||||
|
|
||||||
var dockContentWidth = dockWindow.width
|
|
||||||
var screenWidth = root.screen.width
|
|
||||||
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
|
|
||||||
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
|
|
||||||
|
|
||||||
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: menuContainer
|
|
||||||
|
|
||||||
width: Math.min(400,
|
|
||||||
Math.max(200,
|
|
||||||
menuColumn.implicitWidth + Theme.spacingS * 2))
|
|
||||||
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
|
|
||||||
|
|
||||||
x: {
|
|
||||||
var left = 10
|
|
||||||
var right = root.width - width - 10
|
|
||||||
var want = root.anchorPos.x - width / 2
|
|
||||||
return Math.max(left, Math.min(right, want))
|
|
||||||
}
|
|
||||||
y: Math.max(10, root.anchorPos.y - height + 30)
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
opacity: showContextMenu ? 1 : 0
|
|
||||||
scale: showContextMenu ? 1 : 0.85
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: root.appData
|
|
||||||
&& root.appData.isPinned ? "Unpin from Dock" : "Pin to Dock"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: pinArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!root.appData)
|
|
||||||
return
|
|
||||||
if (root.appData.isPinned) {
|
|
||||||
SessionData.removePinnedApp(root.appData.appId)
|
|
||||||
} else {
|
|
||||||
SessionData.addPinnedApp(root.appData.appId)
|
|
||||||
}
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: root.appData && root.appData.type === "window"
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: root.appData && root.appData.type === "window"
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: root.appData && root.appData.type === "window"
|
|
||||||
width: parent.width
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: closeArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "Close Window"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: closeArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (root.appData && root.appData.toplevelObject) {
|
|
||||||
root.appData.toplevelObject.close()
|
|
||||||
}
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
z: -1
|
|
||||||
onClicked: {
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property string sid: Quickshell.env("XDG_SESSION_ID") || "self"
|
|
||||||
property string sessionPath: ""
|
|
||||||
|
|
||||||
function activate() {
|
|
||||||
loader.activeAsync = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
getSessionPath.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: getSessionPath
|
|
||||||
command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const match = text.match(/objectpath '([^']+)'/)
|
|
||||||
if (match) {
|
|
||||||
root.sessionPath = match[1]
|
|
||||||
console.log("Found session path:", root.sessionPath)
|
|
||||||
checkCurrentLockState.running = true
|
|
||||||
lockStateMonitor.running = true
|
|
||||||
} else {
|
|
||||||
console.warn("Could not determine session path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("Failed to get session path, exit code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: checkCurrentLockState
|
|
||||||
command: root.sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : []
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text.includes("true")) {
|
|
||||||
console.log(
|
|
||||||
"Session is locked on startup, activating lock screen")
|
|
||||||
LockScreenService.resetState()
|
|
||||||
loader.activeAsync = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("Failed to check initial lock state, exit code:",
|
|
||||||
exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: lockStateMonitor
|
|
||||||
command: root.sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath] : []
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
|
|
||||||
onRead: line => {
|
|
||||||
if (line.includes("org.freedesktop.login1.Session.Lock")) {
|
|
||||||
console.log("login1: Lock signal received -> show lock")
|
|
||||||
LockScreenService.resetState()
|
|
||||||
loader.activeAsync = true
|
|
||||||
} else if (line.includes(
|
|
||||||
"org.freedesktop.login1.Session.Unlock")) {
|
|
||||||
console.log("login1: Unlock signal received -> hide lock")
|
|
||||||
loader.active = false
|
|
||||||
} else if (line.includes("LockedHint") && line.includes(
|
|
||||||
"true")) {
|
|
||||||
console.log("login1: LockedHint=true -> show lock")
|
|
||||||
LockScreenService.resetState()
|
|
||||||
loader.activeAsync = true
|
|
||||||
} else if (line.includes("LockedHint") && line.includes(
|
|
||||||
"false")) {
|
|
||||||
console.log("login1: LockedHint=false -> hide lock")
|
|
||||||
loader.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("gdbus monitor failed, exit code:", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: loader
|
|
||||||
|
|
||||||
WlSessionLock {
|
|
||||||
id: sessionLock
|
|
||||||
|
|
||||||
property bool unlocked: false
|
|
||||||
property string sharedPasswordBuffer: ""
|
|
||||||
|
|
||||||
locked: true
|
|
||||||
|
|
||||||
onLockedChanged: {
|
|
||||||
if (!locked)
|
|
||||||
loader.active = false
|
|
||||||
}
|
|
||||||
|
|
||||||
LockSurface {
|
|
||||||
id: lockSurface
|
|
||||||
lock: sessionLock
|
|
||||||
sharedPasswordBuffer: sessionLock.sharedPasswordBuffer
|
|
||||||
onPasswordChanged: newPassword => {
|
|
||||||
sessionLock.sharedPasswordBuffer = newPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LockScreenDemo {
|
|
||||||
id: demoWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: "lock"
|
|
||||||
|
|
||||||
function lock(): void {
|
|
||||||
console.log("Lock screen requested via IPC")
|
|
||||||
LockScreenService.resetState()
|
|
||||||
loader.activeAsync = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function demo(): void {
|
|
||||||
console.log("Lock screen DEMO mode requested via IPC")
|
|
||||||
demoWindow.showDemo()
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLocked(): bool {
|
|
||||||
return loader.active
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,41 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals
|
|
||||||
|
|
||||||
WlSessionLockSurface {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property WlSessionLock lock
|
|
||||||
required property string sharedPasswordBuffer
|
|
||||||
|
|
||||||
signal passwordChanged(string newPassword)
|
|
||||||
|
|
||||||
readonly property bool locked: lock && !lock.locked
|
|
||||||
|
|
||||||
function unlock(): void {
|
|
||||||
lock.locked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
ConfirmModal {
|
|
||||||
id: powerConfirmModal
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
sourceComponent: LockScreenContent {
|
|
||||||
demoMode: false
|
|
||||||
powerModal: powerConfirmModal
|
|
||||||
passwordBuffer: root.sharedPasswordBuffer
|
|
||||||
onUnlockRequested: root.unlock()
|
|
||||||
onPasswordBufferChanged: {
|
|
||||||
if (root.sharedPasswordBuffer !== passwordBuffer) {
|
|
||||||
root.passwordChanged(passwordBuffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: listView
|
|
||||||
|
|
||||||
property var keyboardController: null
|
|
||||||
property bool keyboardActive: false
|
|
||||||
property bool autoScrollDisabled: false
|
|
||||||
|
|
||||||
onIsUserScrollingChanged: {
|
|
||||||
if (isUserScrolling && keyboardController
|
|
||||||
&& keyboardController.keyboardNavigationActive) {
|
|
||||||
autoScrollDisabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableAutoScroll() {
|
|
||||||
autoScrollDisabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias count: listView.count
|
|
||||||
property alias listContentHeight: listView.contentHeight
|
|
||||||
|
|
||||||
clip: true
|
|
||||||
model: NotificationService.groupedNotifications
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: positionPreservationTimer
|
|
||||||
interval: 200
|
|
||||||
running: keyboardController
|
|
||||||
&& keyboardController.keyboardNavigationActive
|
|
||||||
&& !autoScrollDisabled
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
if (keyboardController
|
|
||||||
&& keyboardController.keyboardNavigationActive
|
|
||||||
&& !autoScrollDisabled) {
|
|
||||||
keyboardController.ensureVisible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationEmptyState {
|
|
||||||
visible: listView.count === 0
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
onModelChanged: {
|
|
||||||
if (keyboardController && keyboardController.keyboardNavigationActive) {
|
|
||||||
keyboardController.rebuildFlatNavigation()
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (keyboardController
|
|
||||||
&& keyboardController.keyboardNavigationActive
|
|
||||||
&& !autoScrollDisabled) {
|
|
||||||
keyboardController.ensureVisible()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
readonly property bool isExpanded: NotificationService.expandedGroups[modelData?.key]
|
|
||||||
|| false
|
|
||||||
|
|
||||||
width: ListView.view.width
|
|
||||||
height: notificationCardWrapper.height
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: notificationCardWrapper
|
|
||||||
width: parent.width
|
|
||||||
height: notificationCard.height
|
|
||||||
|
|
||||||
NotificationCard {
|
|
||||||
id: notificationCard
|
|
||||||
width: parent.width
|
|
||||||
notificationGroup: modelData
|
|
||||||
|
|
||||||
isGroupSelected: {
|
|
||||||
if (!keyboardController
|
|
||||||
|| !keyboardController.keyboardNavigationActive)
|
|
||||||
return false
|
|
||||||
keyboardController.selectionVersion
|
|
||||||
if (!listView.keyboardActive)
|
|
||||||
return false
|
|
||||||
const selection = keyboardController.getCurrentSelection()
|
|
||||||
return selection.type === "group"
|
|
||||||
&& selection.groupIndex === index
|
|
||||||
}
|
|
||||||
selectedNotificationIndex: {
|
|
||||||
if (!keyboardController
|
|
||||||
|| !keyboardController.keyboardNavigationActive)
|
|
||||||
return -1
|
|
||||||
keyboardController.selectionVersion
|
|
||||||
if (!listView.keyboardActive)
|
|
||||||
return -1
|
|
||||||
const selection = keyboardController.getCurrentSelection()
|
|
||||||
return (selection.type === "notification"
|
|
||||||
&& selection.groupIndex === index) ? selection.notificationIndex : -1
|
|
||||||
}
|
|
||||||
keyboardNavigationActive: listView.keyboardActive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onGroupedNotificationsChanged() {
|
|
||||||
if (keyboardController) {
|
|
||||||
if (keyboardController.isTogglingGroup) {
|
|
||||||
keyboardController.rebuildFlatNavigation()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboardController.rebuildFlatNavigation()
|
|
||||||
|
|
||||||
if (keyboardController.keyboardNavigationActive) {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (!autoScrollDisabled) {
|
|
||||||
keyboardController.ensureVisible()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onExpandedGroupsChanged() {
|
|
||||||
if (keyboardController
|
|
||||||
&& keyboardController.keyboardNavigationActive) {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (!autoScrollDisabled) {
|
|
||||||
keyboardController.ensureVisible()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onExpandedMessagesChanged() {
|
|
||||||
if (keyboardController
|
|
||||||
&& keyboardController.keyboardNavigationActive) {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (!autoScrollDisabled) {
|
|
||||||
keyboardController.ensureVisible()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: NotificationService
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Notifications.Center
|
|
||||||
|
|
||||||
DankPopout {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool notificationHistoryVisible: false
|
|
||||||
property string triggerSection: "right"
|
|
||||||
property var triggerScreen: null
|
|
||||||
|
|
||||||
NotificationKeyboardController {
|
|
||||||
id: keyboardController
|
|
||||||
listView: null
|
|
||||||
isOpen: notificationHistoryVisible
|
|
||||||
onClose: function () {
|
|
||||||
notificationHistoryVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
triggerX = x
|
|
||||||
triggerY = y
|
|
||||||
triggerWidth = width
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
}
|
|
||||||
|
|
||||||
popupWidth: 400
|
|
||||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
|
|
||||||
triggerX: Screen.width - 400 - Theme.spacingL
|
|
||||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
|
||||||
triggerWidth: 40
|
|
||||||
positioning: "center"
|
|
||||||
WlrLayershell.namespace: "quickshell-notifications"
|
|
||||||
screen: triggerScreen
|
|
||||||
shouldBeVisible: notificationHistoryVisible
|
|
||||||
visible: shouldBeVisible
|
|
||||||
|
|
||||||
onNotificationHistoryVisibleChanged: {
|
|
||||||
if (notificationHistoryVisible) {
|
|
||||||
open()
|
|
||||||
} else {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onShouldBeVisibleChanged: {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
NotificationService.onOverlayOpen()
|
|
||||||
// Set up keyboard controller when content is loaded
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item) {
|
|
||||||
contentLoader.item.externalKeyboardController = keyboardController
|
|
||||||
|
|
||||||
// Find the notification list and set up the connection
|
|
||||||
var notificationList = findChild(contentLoader.item,
|
|
||||||
"notificationList")
|
|
||||||
var notificationHeader = findChild(contentLoader.item,
|
|
||||||
"notificationHeader")
|
|
||||||
|
|
||||||
if (notificationList) {
|
|
||||||
keyboardController.listView = notificationList
|
|
||||||
notificationList.keyboardController = keyboardController
|
|
||||||
}
|
|
||||||
if (notificationHeader) {
|
|
||||||
notificationHeader.keyboardController = keyboardController
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboardController.reset()
|
|
||||||
keyboardController.rebuildFlatNavigation()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
NotificationService.onOverlayClose()
|
|
||||||
// Reset keyboard state when closing
|
|
||||||
keyboardController.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findChild(parent, objectName) {
|
|
||||||
if (parent.objectName === objectName) {
|
|
||||||
return parent
|
|
||||||
}
|
|
||||||
for (var i = 0; i < parent.children.length; i++) {
|
|
||||||
var child = parent.children[i]
|
|
||||||
var result = findChild(child, objectName)
|
|
||||||
if (result) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Rectangle {
|
|
||||||
id: notificationContent
|
|
||||||
|
|
||||||
property var externalKeyboardController: null
|
|
||||||
property real cachedHeaderHeight: 32
|
|
||||||
|
|
||||||
implicitHeight: {
|
|
||||||
let baseHeight = Theme.spacingL * 2
|
|
||||||
baseHeight += cachedHeaderHeight
|
|
||||||
baseHeight += (notificationSettings.expanded ? notificationSettings.contentHeight : 0)
|
|
||||||
baseHeight += Theme.spacingM * 2
|
|
||||||
let listHeight = notificationList.listContentHeight
|
|
||||||
if (NotificationService.groupedNotifications.length === 0)
|
|
||||||
listHeight = 200
|
|
||||||
baseHeight += Math.min(listHeight, 600)
|
|
||||||
return Math.max(
|
|
||||||
300, Math.min(
|
|
||||||
baseHeight,
|
|
||||||
root.screen ? root.screen.height * 0.8 : Screen.height * 0.8))
|
|
||||||
}
|
|
||||||
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (externalKeyboardController) {
|
|
||||||
externalKeyboardController.handleKey(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
Qt.callLater(function () {
|
|
||||||
notificationContent.forceActiveFocus()
|
|
||||||
})
|
|
||||||
else
|
|
||||||
notificationContent.focus = false
|
|
||||||
}
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: contentColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumnInner
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
NotificationHeader {
|
|
||||||
id: notificationHeader
|
|
||||||
objectName: "notificationHeader"
|
|
||||||
onHeightChanged: notificationContent.cachedHeaderHeight = height
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationSettings {
|
|
||||||
id: notificationSettings
|
|
||||||
expanded: notificationHeader.showSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyboardNavigatedNotificationList {
|
|
||||||
id: notificationList
|
|
||||||
objectName: "notificationList"
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - notificationContent.cachedHeaderHeight
|
|
||||||
- notificationSettings.height - contentColumnInner.spacing * 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationKeyboardHints {
|
|
||||||
id: keyboardHints
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
showHints: externalKeyboardController ? externalKeyboardController.showKeyboardHints : false
|
|
||||||
z: 200
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 180
|
|
||||||
easing.type: Easing.OutQuart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +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: "%"
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (DisplayService.brightnessAvailable)
|
|
||||||
value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
root.brightnessDebounceTimer.pendingValue = newValue
|
|
||||||
root.brightnessDebounceTimer.restart()
|
|
||||||
resetHideTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onContainsMouseChanged: {
|
|
||||||
setChildHovered(containsMouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderDragFinished: function(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) {
|
|
||||||
let slider = contentLoader.item.children[0].children[1]
|
|
||||||
if (slider)
|
|
||||||
slider.value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +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
|
|
||||||
|
|
||||||
function onVolumeChanged() {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
resetHideTimer()
|
|
||||||
}
|
|
||||||
onContainsMouseChanged: {
|
|
||||||
setChildHovered(containsMouse || volumeSlider.containsMouse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: volumeSlider
|
|
||||||
|
|
||||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
|
||||||
height: 40
|
|
||||||
x: parent.gap * 2 + Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
enabled: AudioService.sink && AudioService.sink.audio
|
|
||||||
showValue: true
|
|
||||||
unit: "%"
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio)
|
|
||||||
value = Math.round(AudioService.sink.audio.volume * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.volume = newValue / 100
|
|
||||||
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.round(AudioService.sink.audio.volume * 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOsdShown: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio && contentLoader.item) {
|
|
||||||
let slider = contentLoader.item.children[0].children[1]
|
|
||||||
if (slider)
|
|
||||||
slider.value = Math.round(AudioService.sink.audio.volume * 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,491 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["cpu", "memory", "system"])
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["cpu", "memory", "system"])
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM * 2) / 3
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (DgopService.sortBy === "cpu")
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.16)
|
|
||||||
else if (cpuCardMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08)
|
|
||||||
}
|
|
||||||
border.color: DgopService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.4) : Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.2)
|
|
||||||
border.width: DgopService.sortBy === "cpu" ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cpuCardMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: DgopService.setSortBy("cpu")
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "CPU"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: DgopService.sortBy === "cpu" ? Theme.primary : Theme.secondary
|
|
||||||
opacity: DgopService.sortBy === "cpu" ? 1 : 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.cpuUsage === undefined
|
|
||||||
|| DgopService.cpuUsage === null)
|
|
||||||
return "--%"
|
|
||||||
return DgopService.cpuUsage.toFixed(1) + "%"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 1
|
|
||||||
height: 20
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.3)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.cpuTemperature === undefined
|
|
||||||
|| DgopService.cpuTemperature === null
|
|
||||||
|| DgopService.cpuTemperature <= 0)
|
|
||||||
return "--°"
|
|
||||||
|
|
||||||
return Math.round(DgopService.cpuTemperature) + "°"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: {
|
|
||||||
if (DgopService.cpuTemperature > 80)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
if (DgopService.cpuTemperature > 60)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.cpuCores + " cores"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM * 2) / 3
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (DgopService.sortBy === "memory")
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.16)
|
|
||||||
else if (memoryCardMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.secondary.r, Theme.secondary.g,
|
|
||||||
Theme.secondary.b, 0.12)
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.secondary.r, Theme.secondary.g,
|
|
||||||
Theme.secondary.b, 0.08)
|
|
||||||
}
|
|
||||||
border.color: DgopService.sortBy === "memory" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.4) : Qt.rgba(
|
|
||||||
Theme.secondary.r,
|
|
||||||
Theme.secondary.g,
|
|
||||||
Theme.secondary.b,
|
|
||||||
0.2)
|
|
||||||
border.width: DgopService.sortBy === "memory" ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: memoryCardMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: DgopService.setSortBy("memory")
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Memory"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: DgopService.sortBy === "memory" ? Theme.primary : Theme.secondary
|
|
||||||
opacity: DgopService.sortBy === "memory" ? 1 : 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.formatSystemMemory(
|
|
||||||
DgopService.usedMemoryKB)
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 1
|
|
||||||
height: 20
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.3)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: DgopService.totalSwapKB > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(
|
|
||||||
DgopService.usedSwapKB) : ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: DgopService.totalSwapKB > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.totalSwapKB > 0)
|
|
||||||
return "of " + DgopService.formatSystemMemory(
|
|
||||||
DgopService.totalMemoryKB) + " + swap"
|
|
||||||
|
|
||||||
return "of " + DgopService.formatSystemMemory(
|
|
||||||
DgopService.totalMemoryKB)
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM * 2) / 3
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (!DgopService.availableGpus
|
|
||||||
|| DgopService.availableGpus.length === 0) {
|
|
||||||
if (gpuCardMouseArea.containsMouse
|
|
||||||
&& DgopService.availableGpus.length > 1)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.16)
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
var gpu = DgopService.availableGpus[Math.min(
|
|
||||||
SessionData.selectedGpuIndex,
|
|
||||||
DgopService.availableGpus.length - 1)]
|
|
||||||
var vendor = gpu.vendor.toLowerCase()
|
|
||||||
if (vendor.includes("nvidia")) {
|
|
||||||
if (gpuCardMouseArea.containsMouse
|
|
||||||
&& DgopService.availableGpus.length > 1)
|
|
||||||
return Qt.rgba(Theme.success.r, Theme.success.g,
|
|
||||||
Theme.success.b, 0.2)
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.success.r, Theme.success.g,
|
|
||||||
Theme.success.b, 0.12)
|
|
||||||
} else if (vendor.includes("amd")) {
|
|
||||||
if (gpuCardMouseArea.containsMouse
|
|
||||||
&& DgopService.availableGpus.length > 1)
|
|
||||||
return Qt.rgba(Theme.error.r, Theme.error.g,
|
|
||||||
Theme.error.b, 0.2)
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.error.r, Theme.error.g,
|
|
||||||
Theme.error.b, 0.12)
|
|
||||||
} else if (vendor.includes("intel")) {
|
|
||||||
if (gpuCardMouseArea.containsMouse
|
|
||||||
&& DgopService.availableGpus.length > 1)
|
|
||||||
return Qt.rgba(Theme.info.r, Theme.info.g,
|
|
||||||
Theme.info.b, 0.2)
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.info.r, Theme.info.g,
|
|
||||||
Theme.info.b, 0.12)
|
|
||||||
}
|
|
||||||
if (gpuCardMouseArea.containsMouse
|
|
||||||
&& DgopService.availableGpus.length > 1)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.16)
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
if (!DgopService.availableGpus
|
|
||||||
|| DgopService.availableGpus.length === 0)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.2)
|
|
||||||
|
|
||||||
var gpu = DgopService.availableGpus[Math.min(
|
|
||||||
SessionData.selectedGpuIndex,
|
|
||||||
DgopService.availableGpus.length - 1)]
|
|
||||||
var vendor = gpu.vendor.toLowerCase()
|
|
||||||
if (vendor.includes("nvidia"))
|
|
||||||
return Qt.rgba(Theme.success.r, Theme.success.g,
|
|
||||||
Theme.success.b, 0.3)
|
|
||||||
else if (vendor.includes("amd"))
|
|
||||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3)
|
|
||||||
else if (vendor.includes("intel"))
|
|
||||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.2)
|
|
||||||
}
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gpuCardMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
cursorShape: DgopService.availableGpus.length
|
|
||||||
> 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
if (DgopService.availableGpus.length > 1) {
|
|
||||||
var nextIndex = (SessionData.selectedGpuIndex + 1)
|
|
||||||
% DgopService.availableGpus.length
|
|
||||||
SessionData.setSelectedGpuIndex(nextIndex)
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
gpuContextMenu.popup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "GPU"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.secondary
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!DgopService.availableGpus
|
|
||||||
|| DgopService.availableGpus.length === 0)
|
|
||||||
return "No GPU"
|
|
||||||
|
|
||||||
var gpu = DgopService.availableGpus[Math.min(
|
|
||||||
SessionData.selectedGpuIndex,
|
|
||||||
DgopService.availableGpus.length - 1)]
|
|
||||||
// Check if temperature monitoring is enabled for this GPU
|
|
||||||
var tempEnabled = SessionData.enabledGpuPciIds
|
|
||||||
&& SessionData.enabledGpuPciIds.indexOf(
|
|
||||||
gpu.pciId) !== -1
|
|
||||||
var temp = gpu.temperature
|
|
||||||
var hasTemp = tempEnabled && temp !== undefined
|
|
||||||
&& temp !== null && temp !== 0
|
|
||||||
if (hasTemp)
|
|
||||||
return Math.round(temp) + "°"
|
|
||||||
else
|
|
||||||
return gpu.vendor
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: {
|
|
||||||
if (!DgopService.availableGpus
|
|
||||||
|| DgopService.availableGpus.length === 0)
|
|
||||||
return Theme.surfaceText
|
|
||||||
|
|
||||||
var gpu = DgopService.availableGpus[Math.min(
|
|
||||||
SessionData.selectedGpuIndex,
|
|
||||||
DgopService.availableGpus.length - 1)]
|
|
||||||
var tempEnabled = SessionData.enabledGpuPciIds
|
|
||||||
&& SessionData.enabledGpuPciIds.indexOf(
|
|
||||||
gpu.pciId) !== -1
|
|
||||||
var temp = gpu.temperature || 0
|
|
||||||
if (tempEnabled && temp > 80)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
if (tempEnabled && temp > 60)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!DgopService.availableGpus
|
|
||||||
|| DgopService.availableGpus.length === 0)
|
|
||||||
return "No GPUs detected"
|
|
||||||
|
|
||||||
var gpu = DgopService.availableGpus[Math.min(
|
|
||||||
SessionData.selectedGpuIndex,
|
|
||||||
DgopService.availableGpus.length - 1)]
|
|
||||||
var tempEnabled = SessionData.enabledGpuPciIds
|
|
||||||
&& SessionData.enabledGpuPciIds.indexOf(
|
|
||||||
gpu.pciId) !== -1
|
|
||||||
var temp = gpu.temperature
|
|
||||||
var hasTemp = tempEnabled && temp !== undefined
|
|
||||||
&& temp !== null && temp !== 0
|
|
||||||
if (hasTemp)
|
|
||||||
return gpu.vendor + " " + gpu.displayName
|
|
||||||
else
|
|
||||||
return gpu.displayName
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.family: SettingsData.monoFontFamily
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
width: parent.parent.width - Theme.spacingM * 2
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: gpuContextMenu
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: "Enable GPU Temperature"
|
|
||||||
checkable: true
|
|
||||||
checked: {
|
|
||||||
if (!DgopService.availableGpus
|
|
||||||
|| DgopService.availableGpus.length === 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var gpu = DgopService.availableGpus[Math.min(
|
|
||||||
SessionData.selectedGpuIndex,
|
|
||||||
DgopService.availableGpus.length - 1)]
|
|
||||||
if (!gpu.pciId)
|
|
||||||
return false
|
|
||||||
|
|
||||||
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(
|
|
||||||
gpu.pciId) !== -1 : false
|
|
||||||
}
|
|
||||||
onTriggered: {
|
|
||||||
if (!DgopService.availableGpus
|
|
||||||
|| DgopService.availableGpus.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var gpu = DgopService.availableGpus[Math.min(
|
|
||||||
SessionData.selectedGpuIndex,
|
|
||||||
DgopService.availableGpus.length - 1)]
|
|
||||||
if (!gpu.pciId)
|
|
||||||
return
|
|
||||||
|
|
||||||
var enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice(
|
|
||||||
) : []
|
|
||||||
var index = enabledIds.indexOf(gpu.pciId)
|
|
||||||
|
|
||||||
if (checked && index === -1) {
|
|
||||||
enabledIds.push(gpu.pciId)
|
|
||||||
DgopService.addGpuPciId(gpu.pciId)
|
|
||||||
} else if (!checked && index !== -1) {
|
|
||||||
enabledIds.splice(index, 1)
|
|
||||||
DgopService.removeGpuPciId(gpu.pciId)
|
|
||||||
}
|
|
||||||
|
|
||||||
SessionData.setEnabledGpuPciIds(enabledIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,535 +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: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: 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: "DankMaterialShell"
|
|
||||||
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: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: 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: "About"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `DankMaterialShell is a modern desktop inspired by <a href="https://m3.material.io/" style="text-decoration:none; color:${Theme.primary};">MUI 3</a>.
|
|
||||||
<br /><br/>The goal is to provide a high level of functionality and customization so that it can be a suitable replacement for complete desktop environments like Gnome, KDE, or Cosmic.
|
|
||||||
`
|
|
||||||
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: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: 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: "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: "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: "Language:"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "QML (Qt Modeling Language)"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "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: "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: "- Support Us With a Star ⭐"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "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: "- Stateless System Monitoring"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: 1
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: displaysTab
|
|
||||||
|
|
||||||
property var variantComponents: [{
|
|
||||||
"id": "topBar",
|
|
||||||
"name": "Top Bar",
|
|
||||||
"description": "System bar with widgets and system information",
|
|
||||||
"icon": "toolbar"
|
|
||||||
}, {
|
|
||||||
"id": "dock",
|
|
||||||
"name": "Application Dock",
|
|
||||||
"description": "Bottom dock for pinned and running applications",
|
|
||||||
"icon": "dock"
|
|
||||||
}, {
|
|
||||||
"id": "notifications",
|
|
||||||
"name": "Notification Popups",
|
|
||||||
"description": "Notification toast popups",
|
|
||||||
"icon": "notifications"
|
|
||||||
}, {
|
|
||||||
"id": "wallpaper",
|
|
||||||
"name": "Wallpaper",
|
|
||||||
"description": "Desktop background images",
|
|
||||||
"icon": "wallpaper"
|
|
||||||
}, {
|
|
||||||
"id": "osd",
|
|
||||||
"name": "On-Screen Displays",
|
|
||||||
"description": "Volume, brightness, and other system OSDs",
|
|
||||||
"icon": "picture_in_picture"
|
|
||||||
}, {
|
|
||||||
"id": "toast",
|
|
||||||
"name": "Toast Messages",
|
|
||||||
"description": "System toast notifications",
|
|
||||||
"icon": "campaign"
|
|
||||||
}]
|
|
||||||
|
|
||||||
function getScreenPreferences(componentId) {
|
|
||||||
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setScreenPreferences(componentId, screenNames) {
|
|
||||||
var prefs = SettingsData.screenPreferences || {
|
|
||||||
};
|
|
||||||
prefs[componentId] = screenNames;
|
|
||||||
SettingsData.setScreenPreferences(prefs);
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: screensInfoSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: screensInfoSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "monitor"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Connected Displays"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Configure which displays show shell components"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Available Screens (" + Quickshell.screens.length + ")"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Quickshell.screens
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: screenRow.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: screenRow
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "desktop_windows"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM * 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS / 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.width + "×" + modelData.height
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "•"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.model || "Unknown Model"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: displaysTab.variantComponents
|
|
||||||
|
|
||||||
delegate: StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: componentSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: componentSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: modelData.icon
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.description
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show on screens:"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
property string componentId: modelData.id
|
|
||||||
property var selectedScreens: displaysTab.getScreenPreferences(componentId)
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "All displays"
|
|
||||||
description: "Show on all connected displays"
|
|
||||||
checked: parent.selectedScreens.includes("all")
|
|
||||||
onToggled: (checked) => {
|
|
||||||
if (checked)
|
|
||||||
displaysTab.setScreenPreferences(parent.componentId, ["all"]);
|
|
||||||
else
|
|
||||||
displaysTab.setScreenPreferences(parent.componentId, []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.2
|
|
||||||
visible: !parent.selectedScreens.includes("all")
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: !parent.selectedScreens.includes("all")
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Quickshell.screens
|
|
||||||
|
|
||||||
delegate: DankToggle {
|
|
||||||
property string screenName: modelData.name
|
|
||||||
property string componentId: parent.parent.componentId
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
text: screenName
|
|
||||||
description: modelData.width + "×" + modelData.height + " • " + (modelData.model || "Unknown Model")
|
|
||||||
checked: {
|
|
||||||
var prefs = displaysTab.getScreenPreferences(componentId);
|
|
||||||
return !prefs.includes("all") && prefs.includes(screenName);
|
|
||||||
}
|
|
||||||
onToggled: (checked) => {
|
|
||||||
var currentPrefs = displaysTab.getScreenPreferences(componentId);
|
|
||||||
if (currentPrefs.includes("all"))
|
|
||||||
currentPrefs = [];
|
|
||||||
|
|
||||||
var newPrefs = currentPrefs.slice();
|
|
||||||
if (checked) {
|
|
||||||
if (!newPrefs.includes(screenName))
|
|
||||||
newPrefs.push(screenName);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
var index = newPrefs.indexOf(screenName);
|
|
||||||
if (index > -1)
|
|
||||||
newPrefs.splice(index, 1);
|
|
||||||
|
|
||||||
}
|
|
||||||
displaysTab.setScreenPreferences(componentId, newPrefs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: dockTab
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// Enable Dock
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: enableDockSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: enableDockSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "dock_to_bottom"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- enableToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show Dock"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Display a dock at the bottom of the screen with pinned and running applications"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: enableToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.showDock
|
|
||||||
onToggled: checked => {
|
|
||||||
SettingsData.setShowDock(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-hide Dock
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: autoHideSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
visible: SettingsData.showDock
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: autoHideSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "visibility_off"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- autoHideToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Auto-hide Dock"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Hide the dock when not in use and reveal it when hovering near the bottom of the screen"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: autoHideToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.dockAutoHide
|
|
||||||
onToggled: checked => {
|
|
||||||
SettingsData.setDockAutoHide(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dock Transparency Section
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: transparencySection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
visible: SettingsData.showDock
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: transparencySection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "opacity"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Dock Transparency"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
value: Math.round(SettingsData.dockTransparency * 100)
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: "%"
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
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,251 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: recentAppsTab
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: recentlyUsedSection
|
|
||||||
|
|
||||||
property var rankedAppsModel: {
|
|
||||||
var apps = []
|
|
||||||
for (var appId in (AppUsageHistoryData.appUsageRanking
|
|
||||||
|| {})) {
|
|
||||||
var appData = (AppUsageHistoryData.appUsageRanking
|
|
||||||
|| {})[appId]
|
|
||||||
apps.push({
|
|
||||||
"id": appId,
|
|
||||||
"name": appData.name,
|
|
||||||
"exec": appData.exec,
|
|
||||||
"icon": appData.icon,
|
|
||||||
"comment": appData.comment,
|
|
||||||
"usageCount": appData.usageCount,
|
|
||||||
"lastUsed": appData.lastUsed
|
|
||||||
})
|
|
||||||
}
|
|
||||||
apps.sort(function (a, b) {
|
|
||||||
if (a.usageCount !== b.usageCount)
|
|
||||||
return b.usageCount - a.usageCount
|
|
||||||
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
})
|
|
||||||
return apps.slice(0, 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "history"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Recently Used Apps"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - parent.children[0].width
|
|
||||||
- parent.children[1].width
|
|
||||||
- clearAllButton.width - Theme.spacingM * 3
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: clearAllButton
|
|
||||||
|
|
||||||
iconName: "delete_sweep"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.error
|
|
||||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g,
|
|
||||||
Theme.error.b, 0.12)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: {
|
|
||||||
AppUsageHistoryData.appUsageRanking = {}
|
|
||||||
SettingsData.saveSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Apps are ordered by usage frequency, then last used, then alphabetically."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: rankedAppsList
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: recentlyUsedSection.rankedAppsModel
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: rankedAppsList.width
|
|
||||||
height: 48
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
|
||||||
Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.1)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (index + 1).toString()
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.primary
|
|
||||||
width: 20
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
|
|
||||||
sourceSize.width: 24
|
|
||||||
sourceSize.height: 24
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onStatusChanged: {
|
|
||||||
if (status === Image.Error)
|
|
||||||
source = "image://icon/application-x-executable"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name
|
|
||||||
|| "Unknown App"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!modelData.lastUsed)
|
|
||||||
return "Never used"
|
|
||||||
|
|
||||||
var date = new Date(modelData.lastUsed)
|
|
||||||
var now = new Date()
|
|
||||||
var diffMs = now - date
|
|
||||||
var diffMins = Math.floor(
|
|
||||||
diffMs / (1000 * 60))
|
|
||||||
var diffHours = Math.floor(
|
|
||||||
diffMs / (1000 * 60 * 60))
|
|
||||||
var diffDays = Math.floor(
|
|
||||||
diffMs / (1000 * 60 * 60 * 24))
|
|
||||||
if (diffMins < 1)
|
|
||||||
return "Last launched just now"
|
|
||||||
|
|
||||||
if (diffMins < 60)
|
|
||||||
return "Last launched " + diffMins + " minute"
|
|
||||||
+ (diffMins === 1 ? "" : "s") + " ago"
|
|
||||||
|
|
||||||
if (diffHours < 24)
|
|
||||||
return "Last launched " + diffHours + " hour"
|
|
||||||
+ (diffHours === 1 ? "" : "s") + " ago"
|
|
||||||
|
|
||||||
if (diffDays < 7)
|
|
||||||
return "Last launched " + diffDays + " day"
|
|
||||||
+ (diffDays === 1 ? "" : "s") + " ago"
|
|
||||||
|
|
||||||
return "Last launched " + date.toLocaleDateString()
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
circular: true
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: 16
|
|
||||||
iconColor: Theme.error
|
|
||||||
hoverColor: Qt.rgba(Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b, 0.12)
|
|
||||||
onClicked: {
|
|
||||||
var currentRanking = Object.assign(
|
|
||||||
{},
|
|
||||||
AppUsageHistoryData.appUsageRanking
|
|
||||||
|| {})
|
|
||||||
delete currentRanking[modelData.id]
|
|
||||||
AppUsageHistoryData.appUsageRanking = currentRanking
|
|
||||||
SettingsData.saveSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: recentlyUsedSection.rankedAppsModel.length
|
|
||||||
=== 0 ? "No apps have been launched yet." : ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
visible: recentlyUsedSection.rankedAppsModel.length === 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,941 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: themeColorsTab
|
|
||||||
|
|
||||||
property var cachedFontFamilies: []
|
|
||||||
property var cachedMonoFamilies: []
|
|
||||||
property bool fontsEnumerated: false
|
|
||||||
|
|
||||||
function enumerateFonts() {
|
|
||||||
var fonts = ["Default"]
|
|
||||||
var availableFonts = Qt.fontFamilies()
|
|
||||||
var rootFamilies = []
|
|
||||||
var seenFamilies = new Set()
|
|
||||||
for (var i = 0; i < availableFonts.length; i++) {
|
|
||||||
var fontName = availableFonts[i]
|
|
||||||
if (fontName.startsWith("."))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (fontName === SettingsData.defaultFontFamily)
|
|
||||||
continue
|
|
||||||
|
|
||||||
var rootName = fontName.replace(
|
|
||||||
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
|
|
||||||
"").replace(
|
|
||||||
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
|
|
||||||
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
|
|
||||||
function (match, suffix) {
|
|
||||||
return match
|
|
||||||
}).trim()
|
|
||||||
if (!seenFamilies.has(rootName) && rootName !== "") {
|
|
||||||
seenFamilies.add(rootName)
|
|
||||||
rootFamilies.push(rootName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cachedFontFamilies = fonts.concat(rootFamilies.sort())
|
|
||||||
var monoFonts = ["Default"]
|
|
||||||
var monoFamilies = []
|
|
||||||
var seenMonoFamilies = new Set()
|
|
||||||
for (var j = 0; j < availableFonts.length; j++) {
|
|
||||||
var fontName2 = availableFonts[j]
|
|
||||||
if (fontName2.startsWith("."))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (fontName2 === SettingsData.defaultMonoFontFamily)
|
|
||||||
continue
|
|
||||||
|
|
||||||
var lowerName = fontName2.toLowerCase()
|
|
||||||
if (lowerName.includes("mono") || lowerName.includes(
|
|
||||||
"code") || lowerName.includes(
|
|
||||||
"console") || lowerName.includes(
|
|
||||||
"terminal") || lowerName.includes(
|
|
||||||
"courier") || lowerName.includes(
|
|
||||||
"dejavu sans mono") || lowerName.includes(
|
|
||||||
"jetbrains") || lowerName.includes(
|
|
||||||
"fira") || lowerName.includes(
|
|
||||||
"hack") || lowerName.includes(
|
|
||||||
"source code") || lowerName.includes(
|
|
||||||
"ubuntu mono") || lowerName.includes("cascadia")) {
|
|
||||||
var rootName2 = fontName2.replace(
|
|
||||||
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
|
|
||||||
"").replace(
|
|
||||||
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
|
|
||||||
"").trim()
|
|
||||||
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
|
|
||||||
seenMonoFamilies.add(rootName2)
|
|
||||||
monoFamilies.push(rootName2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!fontsEnumerated) {
|
|
||||||
enumerateFonts()
|
|
||||||
fontsEnumerated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// Theme Color
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: themeSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: themeSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "palette"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Theme Color"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Current Theme: " + (Theme.currentTheme === Theme.dynamic ? "Dynamic" : Theme.getThemeColors(Theme.currentThemeName).name)
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (Theme.currentTheme === Theme.dynamic)
|
|
||||||
return "Wallpaper-based dynamic colors"
|
|
||||||
|
|
||||||
var descriptions = {
|
|
||||||
"blue": "Material blue inspired by modern interfaces",
|
|
||||||
"deepBlue": "Deep blue inspired by material 3",
|
|
||||||
"purple": "Rich purple tones for elegance",
|
|
||||||
"green": "Natural green for productivity",
|
|
||||||
"orange": "Energetic orange for creativity",
|
|
||||||
"red": "Bold red for impact",
|
|
||||||
"cyan": "Cool cyan for tranquility",
|
|
||||||
"pink": "Vibrant pink for expression",
|
|
||||||
"amber": "Warm amber for comfort",
|
|
||||||
"coral": "Soft coral for gentle warmth"
|
|
||||||
}
|
|
||||||
return descriptions[Theme.currentThemeName] || "Select a theme"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: Math.min(parent.width, 400)
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Theme.availableThemeNames.slice(0, 5)
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property string themeName: modelData
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Theme.getThemeColors(themeName).primary
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: (Theme.currentThemeName === themeName
|
|
||||||
&& Theme.currentTheme !== Theme.dynamic) ? 2 : 1
|
|
||||||
scale: (Theme.currentThemeName === themeName
|
|
||||||
&& Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: nameText.contentWidth + Theme.spacingS * 2
|
|
||||||
height: nameText.contentHeight + Theme.spacingXS * 2
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingXS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: mouseArea.containsMouse
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: nameText
|
|
||||||
|
|
||||||
text: Theme.getThemeColors(themeName).name
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
Theme.switchTheme(themeName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Theme.availableThemeNames.slice(5, 10)
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property string themeName: modelData
|
|
||||||
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Theme.getThemeColors(themeName).primary
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: Theme.currentThemeName === themeName ? 2 : 1
|
|
||||||
visible: true
|
|
||||||
scale: Theme.currentThemeName === themeName ? 1.1 : 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: nameText2.contentWidth + Theme.spacingS * 2
|
|
||||||
height: nameText2.contentHeight + Theme.spacingXS * 2
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingXS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: mouseArea2.containsMouse
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: nameText2
|
|
||||||
|
|
||||||
text: Theme.getThemeColors(themeName).name
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea2
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
Theme.switchTheme(themeName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 1
|
|
||||||
height: Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 120
|
|
||||||
height: 40
|
|
||||||
radius: 20
|
|
||||||
color: {
|
|
||||||
if (ToastService.wallpaperErrorStatus === "error"
|
|
||||||
|| ToastService.wallpaperErrorStatus === "matugen_missing")
|
|
||||||
return Qt.rgba(Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b, 0.12)
|
|
||||||
else
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
if (ToastService.wallpaperErrorStatus === "error"
|
|
||||||
|| ToastService.wallpaperErrorStatus === "matugen_missing")
|
|
||||||
return Qt.rgba(Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b, 0.5)
|
|
||||||
else if (Theme.currentThemeName === "dynamic")
|
|
||||||
return Theme.primary
|
|
||||||
else
|
|
||||||
return Theme.outline
|
|
||||||
}
|
|
||||||
border.width: (Theme.currentThemeName === "dynamic") ? 2 : 1
|
|
||||||
scale: (Theme.currentThemeName === "dynamic") ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (ToastService.wallpaperErrorStatus === "error"
|
|
||||||
|| ToastService.wallpaperErrorStatus
|
|
||||||
=== "matugen_missing")
|
|
||||||
return "error"
|
|
||||||
else
|
|
||||||
return "palette"
|
|
||||||
}
|
|
||||||
size: 16
|
|
||||||
color: {
|
|
||||||
if (ToastService.wallpaperErrorStatus === "error"
|
|
||||||
|| ToastService.wallpaperErrorStatus
|
|
||||||
=== "matugen_missing")
|
|
||||||
return Theme.error
|
|
||||||
else
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (ToastService.wallpaperErrorStatus === "error")
|
|
||||||
return "Error"
|
|
||||||
else if (ToastService.wallpaperErrorStatus
|
|
||||||
=== "matugen_missing")
|
|
||||||
return "No matugen"
|
|
||||||
else
|
|
||||||
return "Auto"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: {
|
|
||||||
if (ToastService.wallpaperErrorStatus === "error"
|
|
||||||
|| ToastService.wallpaperErrorStatus
|
|
||||||
=== "matugen_missing")
|
|
||||||
return Theme.error
|
|
||||||
else
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: autoMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (ToastService.wallpaperErrorStatus === "matugen_missing")
|
|
||||||
ToastService.showError(
|
|
||||||
"matugen not found - install matugen package for dynamic theming")
|
|
||||||
else if (ToastService.wallpaperErrorStatus === "error")
|
|
||||||
ToastService.showError(
|
|
||||||
"Wallpaper processing failed - check wallpaper path")
|
|
||||||
else
|
|
||||||
Theme.switchTheme(Theme.dynamic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: autoTooltipText.contentWidth + Theme.spacingM * 2
|
|
||||||
height: autoTooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: autoMouseArea.containsMouse
|
|
||||||
&& (Theme.currentTheme !== Theme.dynamic
|
|
||||||
|| ToastService.wallpaperErrorStatus === "error"
|
|
||||||
|| ToastService.wallpaperErrorStatus
|
|
||||||
=== "matugen_missing")
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: autoTooltipText
|
|
||||||
|
|
||||||
text: {
|
|
||||||
if (ToastService.wallpaperErrorStatus === "matugen_missing")
|
|
||||||
return "Install matugen package for dynamic themes"
|
|
||||||
else
|
|
||||||
return "Dynamic wallpaper-based colors"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: (ToastService.wallpaperErrorStatus === "error"
|
|
||||||
|| ToastService.wallpaperErrorStatus
|
|
||||||
=== "matugen_missing") ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: Math.min(implicitWidth, 250)
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 120
|
|
||||||
height: 40
|
|
||||||
radius: 20
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: (Theme.currentThemeName === "custom") ? Theme.primary : Theme.outline
|
|
||||||
border.width: (Theme.currentThemeName === "custom") ? 2 : 1
|
|
||||||
scale: (Theme.currentThemeName === "custom") ? 1.1 : (customMouseArea.containsMouse ? 1.02 : 1)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "folder_open"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Custom"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: customMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
fileBrowserModal.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: customTooltipText.contentWidth + Theme.spacingM * 2
|
|
||||||
height: customTooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: customMouseArea.containsMouse
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: customTooltipText
|
|
||||||
text: {
|
|
||||||
if (Theme.currentThemeName === "custom")
|
|
||||||
return SettingsData.customThemeFile || "Custom theme loaded"
|
|
||||||
else
|
|
||||||
return "Load custom theme from JSON file"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: Math.min(implicitWidth, 250)
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // Close Row
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transparency Settings
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: transparencySection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: transparencySection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "opacity"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Transparency Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Top Bar Transparency"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
value: Math.round(
|
|
||||||
SettingsData.topBarTransparency * 100)
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: ""
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setTopBarTransparency(
|
|
||||||
newValue / 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Top Bar Widget Transparency"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
value: Math.round(
|
|
||||||
SettingsData.topBarWidgetTransparency * 100)
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: ""
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setTopBarWidgetTransparency(
|
|
||||||
newValue / 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Popup Transparency"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
value: Math.round(
|
|
||||||
SettingsData.popupTransparency * 100)
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: ""
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setPopupTransparency(
|
|
||||||
newValue / 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// System Configuration Warning
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: warningText.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g,
|
|
||||||
Theme.warning.b, 0.12)
|
|
||||||
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g,
|
|
||||||
Theme.warning.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "info"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.warning
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: warningText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
text: "The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0)."
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width - Theme.iconSizeSmall - Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon Theme
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: iconThemeSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: iconThemeSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "image"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "Icon Theme"
|
|
||||||
description: "DankShell & System Icons\n(requires restart)"
|
|
||||||
currentValue: SettingsData.iconTheme
|
|
||||||
enableFuzzySearch: true
|
|
||||||
popupWidthOffset: 100
|
|
||||||
maxPopupHeight: 236
|
|
||||||
options: {
|
|
||||||
SettingsData.detectAvailableIconThemes()
|
|
||||||
return SettingsData.availableIconThemes
|
|
||||||
}
|
|
||||||
onValueChanged: value => {
|
|
||||||
SettingsData.setIconTheme(value)
|
|
||||||
if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" &&
|
|
||||||
Quickshell.env("QT_QPA_PLATFORMTHEME") != "qt6ct" &&
|
|
||||||
Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") != "qt6ct") {
|
|
||||||
ToastService.showError("Missing Environment Variables", "You need to set either:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct\nas environment variables, and then restart the shell.\n\nqt6ct requires qt6ct-kde to be installed.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// System App Theming
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: systemThemingSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
visible: Theme.matugenAvailable
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: systemThemingSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "extension"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "System App Theming"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: 48
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "folder"
|
|
||||||
size: 16
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Apply GTK Colors"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: Theme.applyGtkColors()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: 48
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: 16
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Apply Qt Colors"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: Theme.applyQtColors()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.<br /><br />It is recommended to install <a href="https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming" style="text-decoration:none; color:${Theme.primary};">Colloid</a> GTK theme prior to applying GTK themes.`
|
|
||||||
textFormat: Text.RichText
|
|
||||||
linkColor: Theme.primary
|
|
||||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: fileBrowserModal
|
|
||||||
browserTitle: "Select Custom Theme"
|
|
||||||
filterExtensions: ["*.json"]
|
|
||||||
showHiddenFiles: true
|
|
||||||
|
|
||||||
function selectCustomTheme() {
|
|
||||||
shouldBeVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileSelected: function(filePath) {
|
|
||||||
// Save the custom theme file path and switch to custom theme
|
|
||||||
if (filePath.endsWith(".json")) {
|
|
||||||
SettingsData.setCustomThemeFile(filePath)
|
|
||||||
Theme.switchTheme("custom")
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,384 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: timeTab
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// Time Format
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: timeSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: timeSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "schedule"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- toggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "24-Hour Format"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Use 24-hour time format instead of 12-hour AM/PM"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: toggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.use24HourClock
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setClockFormat(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date Format Section
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: dateSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: dateSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "calendar_today"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Date Format"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
text: "Top Bar Format"
|
|
||||||
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
|
|
||||||
currentValue: {
|
|
||||||
if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0) {
|
|
||||||
return "System Default"
|
|
||||||
}
|
|
||||||
// Find matching preset or show "Custom"
|
|
||||||
const presets = [{
|
|
||||||
"format": "ddd d",
|
|
||||||
"label": "Day Date"
|
|
||||||
}, {
|
|
||||||
"format": "ddd MMM d",
|
|
||||||
"label": "Day Month Date"
|
|
||||||
}, {
|
|
||||||
"format": "MMM d",
|
|
||||||
"label": "Month Date"
|
|
||||||
}, {
|
|
||||||
"format": "M/d",
|
|
||||||
"label": "Numeric (M/D)"
|
|
||||||
}, {
|
|
||||||
"format": "d/M",
|
|
||||||
"label": "Numeric (D/M)"
|
|
||||||
}, {
|
|
||||||
"format": "ddd d MMM yyyy",
|
|
||||||
"label": "Full with Year"
|
|
||||||
}, {
|
|
||||||
"format": "yyyy-MM-dd",
|
|
||||||
"label": "ISO Date"
|
|
||||||
}, {
|
|
||||||
"format": "dddd, MMMM d",
|
|
||||||
"label": "Full Day & Month"
|
|
||||||
}]
|
|
||||||
const match = presets.find(p => {
|
|
||||||
return p.format
|
|
||||||
=== SettingsData.clockDateFormat
|
|
||||||
})
|
|
||||||
return match ? match.label : "Custom: " + SettingsData.clockDateFormat
|
|
||||||
}
|
|
||||||
options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
|
|
||||||
onValueChanged: value => {
|
|
||||||
const formatMap = {
|
|
||||||
"System Default": "",
|
|
||||||
"Day Date": "ddd d",
|
|
||||||
"Day Month Date": "ddd MMM d",
|
|
||||||
"Month Date": "MMM d",
|
|
||||||
"Numeric (M/D)": "M/d",
|
|
||||||
"Numeric (D/M)": "d/M",
|
|
||||||
"Full with Year": "ddd d MMM yyyy",
|
|
||||||
"ISO Date": "yyyy-MM-dd",
|
|
||||||
"Full Day & Month": "dddd, MMMM d"
|
|
||||||
}
|
|
||||||
if (value === "Custom...") {
|
|
||||||
customFormatInput.visible = true
|
|
||||||
} else {
|
|
||||||
customFormatInput.visible = false
|
|
||||||
SettingsData.setClockDateFormat(
|
|
||||||
formatMap[value])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
text: "Lock Screen Format"
|
|
||||||
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))
|
|
||||||
currentValue: {
|
|
||||||
if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0) {
|
|
||||||
return "System Default"
|
|
||||||
}
|
|
||||||
// Find matching preset or show "Custom"
|
|
||||||
const presets = [{
|
|
||||||
"format": "ddd d",
|
|
||||||
"label": "Day Date"
|
|
||||||
}, {
|
|
||||||
"format": "ddd MMM d",
|
|
||||||
"label": "Day Month Date"
|
|
||||||
}, {
|
|
||||||
"format": "MMM d",
|
|
||||||
"label": "Month Date"
|
|
||||||
}, {
|
|
||||||
"format": "M/d",
|
|
||||||
"label": "Numeric (M/D)"
|
|
||||||
}, {
|
|
||||||
"format": "d/M",
|
|
||||||
"label": "Numeric (D/M)"
|
|
||||||
}, {
|
|
||||||
"format": "ddd d MMM yyyy",
|
|
||||||
"label": "Full with Year"
|
|
||||||
}, {
|
|
||||||
"format": "yyyy-MM-dd",
|
|
||||||
"label": "ISO Date"
|
|
||||||
}, {
|
|
||||||
"format": "dddd, MMMM d",
|
|
||||||
"label": "Full Day & Month"
|
|
||||||
}]
|
|
||||||
const match = presets.find(p => {
|
|
||||||
return p.format
|
|
||||||
=== SettingsData.lockDateFormat
|
|
||||||
})
|
|
||||||
return match ? match.label : "Custom: " + SettingsData.lockDateFormat
|
|
||||||
}
|
|
||||||
options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
|
|
||||||
onValueChanged: value => {
|
|
||||||
const formatMap = {
|
|
||||||
"System Default": "",
|
|
||||||
"Day Date": "ddd d",
|
|
||||||
"Day Month Date": "ddd MMM d",
|
|
||||||
"Month Date": "MMM d",
|
|
||||||
"Numeric (M/D)": "M/d",
|
|
||||||
"Numeric (D/M)": "d/M",
|
|
||||||
"Full with Year": "ddd d MMM yyyy",
|
|
||||||
"ISO Date": "yyyy-MM-dd",
|
|
||||||
"Full Day & Month": "dddd, MMMM d"
|
|
||||||
}
|
|
||||||
if (value === "Custom...") {
|
|
||||||
customLockFormatInput.visible = true
|
|
||||||
} else {
|
|
||||||
customLockFormatInput.visible = false
|
|
||||||
SettingsData.setLockDateFormat(
|
|
||||||
formatMap[value])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: customFormatInput
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
visible: false
|
|
||||||
placeholderText: "Enter custom top bar format (e.g., ddd MMM d)"
|
|
||||||
text: SettingsData.clockDateFormat
|
|
||||||
onTextChanged: {
|
|
||||||
if (visible && text)
|
|
||||||
SettingsData.setClockDateFormat(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: customLockFormatInput
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
visible: false
|
|
||||||
placeholderText: "Enter custom lock screen format (e.g., dddd, MMMM d)"
|
|
||||||
text: SettingsData.lockDateFormat
|
|
||||||
onTextChanged: {
|
|
||||||
if (visible && text)
|
|
||||||
SettingsData.setLockDateFormat(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: formatHelp.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.2)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.1)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: formatHelp
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Format Legend"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingL) / 2
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• d - Day (1-31)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• dd - Day (01-31)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• ddd - Day name (Mon)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• dddd - Day name (Monday)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• M - Month (1-12)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingL) / 2
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• MM - Month (01-12)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• MMM - Month (Jan)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• MMMM - Month (January)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• yy - Year (24)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• yyyy - Year (2024)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,267 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: weatherTab
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// Enable Weather
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: enableWeatherSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: enableWeatherSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "cloud"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- enableToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Enable Weather"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show weather information in top bar and control center"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: enableToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.weatherEnabled
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setWeatherEnabled(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temperature Unit
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: temperatureSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
visible: SettingsData.weatherEnabled
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: temperatureSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "thermostat"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- temperatureToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Use Fahrenheit"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Use Fahrenheit instead of Celsius for temperature"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: temperatureToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.useFahrenheit
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setTemperatureUnit(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Location Settings
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: locationSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
visible: SettingsData.weatherEnabled
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: locationSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "location_on"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
||||||
- autoLocationToggle.width - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Auto Location"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Allow wttr.in to determine location based on IP address"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: autoLocationToggle
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: SettingsData.useAutoLocation
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setAutoLocation(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: !SettingsData.useAutoLocation
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Custom Location"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankLocationSearch {
|
|
||||||
width: parent.width
|
|
||||||
currentLocation: SettingsData.weatherLocation
|
|
||||||
placeholderText: "New York, NY"
|
|
||||||
onLocationSelected: (displayName, coordinates) => {
|
|
||||||
SettingsData.setWeatherLocation(
|
|
||||||
displayName,
|
|
||||||
coordinates)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: widgetTweaksTab
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
// Launcher Button Section
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: launcherButtonSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: launcherButtonSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "apps"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Launcher Button"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "Use OS Logo"
|
|
||||||
description: "Display operating system logo instead of apps icon"
|
|
||||||
checked: SettingsData.useOSLogo
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setUseOSLogo(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width - Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
visible: SettingsData.useOSLogo
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: 120
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Color Override"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
width: 100
|
|
||||||
height: 28
|
|
||||||
placeholderText: "#ffffff"
|
|
||||||
text: SettingsData.osLogoColorOverride
|
|
||||||
maximumLength: 7
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
topPadding: Theme.spacingXS
|
|
||||||
bottomPadding: Theme.spacingXS
|
|
||||||
onEditingFinished: {
|
|
||||||
var color = text.trim()
|
|
||||||
if (color === ""
|
|
||||||
|| /^#[0-9A-Fa-f]{6}$/.test(color))
|
|
||||||
SettingsData.setOSLogoColorOverride(
|
|
||||||
color)
|
|
||||||
else
|
|
||||||
text = SettingsData.osLogoColorOverride
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: 120
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Brightness"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: 100
|
|
||||||
height: 20
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
value: Math.round(
|
|
||||||
SettingsData.osLogoBrightness * 100)
|
|
||||||
unit: "%"
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setOSLogoBrightness(
|
|
||||||
newValue / 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: 120
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Contrast"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
width: 100
|
|
||||||
height: 20
|
|
||||||
minimum: 0
|
|
||||||
maximum: 200
|
|
||||||
value: Math.round(
|
|
||||||
SettingsData.osLogoContrast * 100)
|
|
||||||
unit: "%"
|
|
||||||
showValue: true
|
|
||||||
wheelEnabled: false
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
SettingsData.setOSLogoContrast(
|
|
||||||
newValue / 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: workspaceSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: workspaceSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "view_module"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Workspace Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "Workspace Index Numbers"
|
|
||||||
description: "Show workspace index numbers in the top bar workspace switcher"
|
|
||||||
checked: SettingsData.showWorkspaceIndex
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setShowWorkspaceIndex(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "Workspace Padding"
|
|
||||||
description: "Always show a minimum of 3 workspaces, even if fewer are available"
|
|
||||||
checked: SettingsData.showWorkspacePadding
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setShowWorkspacePadding(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: runningAppsSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: runningAppsSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "apps"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Running Apps Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "Running Apps Only In Current Workspace"
|
|
||||||
description: "Show only apps running in current workspace"
|
|
||||||
checked: SettingsData.runningAppsCurrentWorkspace
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setRunningAppsCurrentWorkspace(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
visible: SettingsData.hasNamedWorkspaces()
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: workspaceIconsSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "label"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Named Workspace Icons"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Configure icons for named workspaces. Icons take priority over numbers when both are enabled."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outline
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: SettingsData.getNamedWorkspaces()
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: workspaceIconRow.implicitHeight + Theme.spacingM
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
|
||||||
Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.5)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: workspaceIconRow
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "\"" + modelData + "\""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 150
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIconPicker {
|
|
||||||
id: iconPicker
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
var iconData = SettingsData.getWorkspaceNameIcon(
|
|
||||||
modelData)
|
|
||||||
if (iconData) {
|
|
||||||
setIcon(iconData.value,
|
|
||||||
iconData.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onIconSelected: (iconName, iconType) => {
|
|
||||||
SettingsData.setWorkspaceNameIcon(
|
|
||||||
modelData, {
|
|
||||||
"type": iconType,
|
|
||||||
"value": iconName
|
|
||||||
})
|
|
||||||
setIcon(iconName,
|
|
||||||
iconType)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onWorkspaceIconsUpdated() {
|
|
||||||
var iconData = SettingsData.getWorkspaceNameIcon(
|
|
||||||
modelData)
|
|
||||||
if (iconData) {
|
|
||||||
iconPicker.setIcon(
|
|
||||||
iconData.value,
|
|
||||||
iconData.type)
|
|
||||||
} else {
|
|
||||||
iconPicker.setIcon("", "icon")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer
|
|
||||||
border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "close"
|
|
||||||
size: 16
|
|
||||||
color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clearMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
SettingsData.removeWorkspaceNameIcon(
|
|
||||||
modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,972 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: widgetsTab
|
|
||||||
|
|
||||||
property var baseWidgetDefinitions: [{
|
|
||||||
"id": "launcherButton",
|
|
||||||
"text": "App Launcher",
|
|
||||||
"description": "Quick access to application launcher",
|
|
||||||
"icon": "apps",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "workspaceSwitcher",
|
|
||||||
"text": "Workspace Switcher",
|
|
||||||
"description": "Shows current workspace and allows switching",
|
|
||||||
"icon": "view_module",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "focusedWindow",
|
|
||||||
"text": "Focused Window",
|
|
||||||
"description": "Display currently focused application title",
|
|
||||||
"icon": "window",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "runningApps",
|
|
||||||
"text": "Running Apps",
|
|
||||||
"description": "Shows all running applications with focus indication",
|
|
||||||
"icon": "apps",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "clock",
|
|
||||||
"text": "Clock",
|
|
||||||
"description": "Current time and date display",
|
|
||||||
"icon": "schedule",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "weather",
|
|
||||||
"text": "Weather Widget",
|
|
||||||
"description": "Current weather conditions and temperature",
|
|
||||||
"icon": "wb_sunny",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "music",
|
|
||||||
"text": "Media Controls",
|
|
||||||
"description": "Control currently playing media",
|
|
||||||
"icon": "music_note",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "clipboard",
|
|
||||||
"text": "Clipboard Manager",
|
|
||||||
"description": "Access clipboard history",
|
|
||||||
"icon": "content_paste",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "cpuUsage",
|
|
||||||
"text": "CPU Usage",
|
|
||||||
"description": "CPU usage indicator",
|
|
||||||
"icon": "memory",
|
|
||||||
"enabled": DgopService.dgopAvailable,
|
|
||||||
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
|
|
||||||
}, {
|
|
||||||
"id": "memUsage",
|
|
||||||
"text": "Memory Usage",
|
|
||||||
"description": "Memory usage indicator",
|
|
||||||
"icon": "storage",
|
|
||||||
"enabled": DgopService.dgopAvailable,
|
|
||||||
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
|
|
||||||
}, {
|
|
||||||
"id": "cpuTemp",
|
|
||||||
"text": "CPU Temperature",
|
|
||||||
"description": "CPU temperature display",
|
|
||||||
"icon": "device_thermostat",
|
|
||||||
"enabled": DgopService.dgopAvailable,
|
|
||||||
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
|
|
||||||
}, {
|
|
||||||
"id": "gpuTemp",
|
|
||||||
"text": "GPU Temperature",
|
|
||||||
"description": "GPU temperature display",
|
|
||||||
"icon": "auto_awesome_mosaic",
|
|
||||||
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : "This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.",
|
|
||||||
"enabled": DgopService.dgopAvailable
|
|
||||||
}, {
|
|
||||||
"id": "systemTray",
|
|
||||||
"text": "System Tray",
|
|
||||||
"description": "System notification area icons",
|
|
||||||
"icon": "notifications",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "privacyIndicator",
|
|
||||||
"text": "Privacy Indicator",
|
|
||||||
"description": "Shows when microphone, camera, or screen sharing is active",
|
|
||||||
"icon": "privacy_tip",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "controlCenterButton",
|
|
||||||
"text": "Control Center",
|
|
||||||
"description": "Access to system controls and settings",
|
|
||||||
"icon": "settings",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "notificationButton",
|
|
||||||
"text": "Notification Center",
|
|
||||||
"description": "Access to notifications and do not disturb",
|
|
||||||
"icon": "notifications",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "notepadButton",
|
|
||||||
"text": "Notepad",
|
|
||||||
"description": "Quick access to notepad",
|
|
||||||
"icon": "assignment",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "battery",
|
|
||||||
"text": "Battery",
|
|
||||||
"description": "Battery level and power management",
|
|
||||||
"icon": "battery_std",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "idleInhibitor",
|
|
||||||
"text": "Idle Inhibitor",
|
|
||||||
"description": "Prevent screen timeout",
|
|
||||||
"icon": "motion_sensor_active",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "spacer",
|
|
||||||
"text": "Spacer",
|
|
||||||
"description": "Customizable empty space",
|
|
||||||
"icon": "more_horiz",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "separator",
|
|
||||||
"text": "Separator",
|
|
||||||
"description": "Visual divider between widgets",
|
|
||||||
"icon": "remove",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "network_speed_monitor",
|
|
||||||
"text": "Network Speed Monitor",
|
|
||||||
"description": "Network download and upload speed display",
|
|
||||||
"icon": "network_check",
|
|
||||||
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined,
|
|
||||||
"enabled": DgopService.dgopAvailable
|
|
||||||
}, {
|
|
||||||
"id": "keyboard_layout_name",
|
|
||||||
"text": "Keyboard Layout Name",
|
|
||||||
"description": "Displays the active keyboard layout and allows switching",
|
|
||||||
"icon": "keyboard",
|
|
||||||
}]
|
|
||||||
property var defaultLeftWidgets: [{
|
|
||||||
"id": "launcherButton",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "workspaceSwitcher",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "focusedWindow",
|
|
||||||
"enabled": true
|
|
||||||
}]
|
|
||||||
property var defaultCenterWidgets: [{
|
|
||||||
"id": "music",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "clock",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "weather",
|
|
||||||
"enabled": true
|
|
||||||
}]
|
|
||||||
property var defaultRightWidgets: [{
|
|
||||||
"id": "privacyIndicator",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "systemTray",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "clipboard",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "notificationButton",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "battery",
|
|
||||||
"enabled": true
|
|
||||||
}, {
|
|
||||||
"id": "controlCenterButton",
|
|
||||||
"enabled": true
|
|
||||||
}]
|
|
||||||
|
|
||||||
function addWidgetToSection(widgetId, targetSection) {
|
|
||||||
var widgetObj = {
|
|
||||||
"id": widgetId,
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
if (widgetId === "spacer")
|
|
||||||
widgetObj.size = 20
|
|
||||||
if (widgetId === "gpuTemp") {
|
|
||||||
widgetObj.selectedGpuIndex = 0
|
|
||||||
widgetObj.pciId = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var widgets = []
|
|
||||||
if (targetSection === "left") {
|
|
||||||
widgets = SettingsData.topBarLeftWidgets.slice()
|
|
||||||
widgets.push(widgetObj)
|
|
||||||
SettingsData.setTopBarLeftWidgets(widgets)
|
|
||||||
} else if (targetSection === "center") {
|
|
||||||
widgets = SettingsData.topBarCenterWidgets.slice()
|
|
||||||
widgets.push(widgetObj)
|
|
||||||
SettingsData.setTopBarCenterWidgets(widgets)
|
|
||||||
} else if (targetSection === "right") {
|
|
||||||
widgets = SettingsData.topBarRightWidgets.slice()
|
|
||||||
widgets.push(widgetObj)
|
|
||||||
SettingsData.setTopBarRightWidgets(widgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWidgetFromSection(sectionId, widgetIndex) {
|
|
||||||
var widgets = []
|
|
||||||
if (sectionId === "left") {
|
|
||||||
widgets = SettingsData.topBarLeftWidgets.slice()
|
|
||||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
|
||||||
widgets.splice(widgetIndex, 1)
|
|
||||||
}
|
|
||||||
SettingsData.setTopBarLeftWidgets(widgets)
|
|
||||||
} else if (sectionId === "center") {
|
|
||||||
widgets = SettingsData.topBarCenterWidgets.slice()
|
|
||||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
|
||||||
widgets.splice(widgetIndex, 1)
|
|
||||||
}
|
|
||||||
SettingsData.setTopBarCenterWidgets(widgets)
|
|
||||||
} else if (sectionId === "right") {
|
|
||||||
widgets = SettingsData.topBarRightWidgets.slice()
|
|
||||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
|
||||||
widgets.splice(widgetIndex, 1)
|
|
||||||
}
|
|
||||||
SettingsData.setTopBarRightWidgets(widgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleItemEnabledChanged(sectionId, itemId, enabled) {
|
|
||||||
var widgets = []
|
|
||||||
if (sectionId === "left")
|
|
||||||
widgets = SettingsData.topBarLeftWidgets.slice()
|
|
||||||
else if (sectionId === "center")
|
|
||||||
widgets = SettingsData.topBarCenterWidgets.slice()
|
|
||||||
else if (sectionId === "right")
|
|
||||||
widgets = SettingsData.topBarRightWidgets.slice()
|
|
||||||
for (var i = 0; i < widgets.length; i++) {
|
|
||||||
var widget = widgets[i]
|
|
||||||
var widgetId = typeof widget === "string" ? widget : widget.id
|
|
||||||
if (widgetId === itemId) {
|
|
||||||
if (typeof widget === "string") {
|
|
||||||
widgets[i] = {
|
|
||||||
"id": widget,
|
|
||||||
"enabled": enabled
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var newWidget = {
|
|
||||||
"id": widget.id,
|
|
||||||
"enabled": enabled
|
|
||||||
}
|
|
||||||
if (widget.size !== undefined)
|
|
||||||
newWidget.size = widget.size
|
|
||||||
if (widget.selectedGpuIndex !== undefined)
|
|
||||||
newWidget.selectedGpuIndex = widget.selectedGpuIndex
|
|
||||||
else if (widget.id === "gpuTemp")
|
|
||||||
newWidget.selectedGpuIndex = 0
|
|
||||||
if (widget.pciId !== undefined)
|
|
||||||
newWidget.pciId = widget.pciId
|
|
||||||
else if (widget.id === "gpuTemp")
|
|
||||||
newWidget.pciId = ""
|
|
||||||
widgets[i] = newWidget
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sectionId === "left")
|
|
||||||
SettingsData.setTopBarLeftWidgets(widgets)
|
|
||||||
else if (sectionId === "center")
|
|
||||||
SettingsData.setTopBarCenterWidgets(widgets)
|
|
||||||
else if (sectionId === "right")
|
|
||||||
SettingsData.setTopBarRightWidgets(widgets)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleItemOrderChanged(sectionId, newOrder) {
|
|
||||||
if (sectionId === "left")
|
|
||||||
SettingsData.setTopBarLeftWidgets(newOrder)
|
|
||||||
else if (sectionId === "center")
|
|
||||||
SettingsData.setTopBarCenterWidgets(newOrder)
|
|
||||||
else if (sectionId === "right")
|
|
||||||
SettingsData.setTopBarRightWidgets(newOrder)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSpacerSizeChanged(sectionId, itemId, newSize) {
|
|
||||||
var widgets = []
|
|
||||||
if (sectionId === "left")
|
|
||||||
widgets = SettingsData.topBarLeftWidgets.slice()
|
|
||||||
else if (sectionId === "center")
|
|
||||||
widgets = SettingsData.topBarCenterWidgets.slice()
|
|
||||||
else if (sectionId === "right")
|
|
||||||
widgets = SettingsData.topBarRightWidgets.slice()
|
|
||||||
for (var i = 0; i < widgets.length; i++) {
|
|
||||||
var widget = widgets[i]
|
|
||||||
var widgetId = typeof widget === "string" ? widget : widget.id
|
|
||||||
if (widgetId === itemId && widgetId === "spacer") {
|
|
||||||
if (typeof widget === "string") {
|
|
||||||
widgets[i] = {
|
|
||||||
"id": widget,
|
|
||||||
"enabled": true,
|
|
||||||
"size": newSize
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var newWidget = {
|
|
||||||
"id": widget.id,
|
|
||||||
"enabled": widget.enabled,
|
|
||||||
"size": newSize
|
|
||||||
}
|
|
||||||
if (widget.selectedGpuIndex !== undefined)
|
|
||||||
newWidget.selectedGpuIndex = widget.selectedGpuIndex
|
|
||||||
if (widget.pciId !== undefined)
|
|
||||||
newWidget.pciId = widget.pciId
|
|
||||||
widgets[i] = newWidget
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sectionId === "left")
|
|
||||||
SettingsData.setTopBarLeftWidgets(widgets)
|
|
||||||
else if (sectionId === "center")
|
|
||||||
SettingsData.setTopBarCenterWidgets(widgets)
|
|
||||||
else if (sectionId === "right")
|
|
||||||
SettingsData.setTopBarRightWidgets(widgets)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGpuSelectionChanged(sectionId, widgetIndex, selectedGpuIndex) {
|
|
||||||
var widgets = []
|
|
||||||
if (sectionId === "left")
|
|
||||||
widgets = SettingsData.topBarLeftWidgets.slice()
|
|
||||||
else if (sectionId === "center")
|
|
||||||
widgets = SettingsData.topBarCenterWidgets.slice()
|
|
||||||
else if (sectionId === "right")
|
|
||||||
widgets = SettingsData.topBarRightWidgets.slice()
|
|
||||||
|
|
||||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
|
||||||
var widget = widgets[widgetIndex]
|
|
||||||
if (typeof widget === "string") {
|
|
||||||
widgets[widgetIndex] = {
|
|
||||||
"id": widget,
|
|
||||||
"enabled": true,
|
|
||||||
"selectedGpuIndex": selectedGpuIndex,
|
|
||||||
"pciId": DgopService.availableGpus
|
|
||||||
&& DgopService.availableGpus.length
|
|
||||||
> selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : ""
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var newWidget = {
|
|
||||||
"id": widget.id,
|
|
||||||
"enabled": widget.enabled,
|
|
||||||
"selectedGpuIndex": selectedGpuIndex,
|
|
||||||
"pciId": DgopService.availableGpus
|
|
||||||
&& DgopService.availableGpus.length
|
|
||||||
> selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : ""
|
|
||||||
}
|
|
||||||
if (widget.size !== undefined)
|
|
||||||
newWidget.size = widget.size
|
|
||||||
widgets[widgetIndex] = newWidget
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sectionId === "left")
|
|
||||||
SettingsData.setTopBarLeftWidgets(widgets)
|
|
||||||
else if (sectionId === "center")
|
|
||||||
SettingsData.setTopBarCenterWidgets(widgets)
|
|
||||||
else if (sectionId === "right")
|
|
||||||
SettingsData.setTopBarRightWidgets(widgets)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemsForSection(sectionId) {
|
|
||||||
var widgets = []
|
|
||||||
var widgetData = []
|
|
||||||
if (sectionId === "left")
|
|
||||||
widgetData = SettingsData.topBarLeftWidgets || []
|
|
||||||
else if (sectionId === "center")
|
|
||||||
widgetData = SettingsData.topBarCenterWidgets || []
|
|
||||||
else if (sectionId === "right")
|
|
||||||
widgetData = SettingsData.topBarRightWidgets || []
|
|
||||||
widgetData.forEach(widget => {
|
|
||||||
var widgetId = typeof widget === "string" ? widget : widget.id
|
|
||||||
var widgetEnabled = typeof widget
|
|
||||||
=== "string" ? true : widget.enabled
|
|
||||||
var widgetSize = typeof widget === "string" ? undefined : widget.size
|
|
||||||
var widgetSelectedGpuIndex = typeof widget
|
|
||||||
=== "string" ? undefined : widget.selectedGpuIndex
|
|
||||||
var widgetPciId = typeof widget
|
|
||||||
=== "string" ? undefined : widget.pciId
|
|
||||||
var widgetDef = baseWidgetDefinitions.find(w => {
|
|
||||||
return w.id === widgetId
|
|
||||||
})
|
|
||||||
if (widgetDef) {
|
|
||||||
var item = Object.assign({}, widgetDef)
|
|
||||||
item.enabled = widgetEnabled
|
|
||||||
if (widgetSize !== undefined)
|
|
||||||
item.size = widgetSize
|
|
||||||
if (widgetSelectedGpuIndex !== undefined)
|
|
||||||
item.selectedGpuIndex = widgetSelectedGpuIndex
|
|
||||||
if (widgetPciId !== undefined)
|
|
||||||
item.pciId = widgetPciId
|
|
||||||
|
|
||||||
widgets.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return widgets
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
// Only set defaults if widgets have never been configured (null/undefined, not empty array)
|
|
||||||
if (!SettingsData.topBarLeftWidgets)
|
|
||||||
SettingsData.setTopBarLeftWidgets(defaultLeftWidgets)
|
|
||||||
|
|
||||||
if (!SettingsData.topBarCenterWidgets)
|
|
||||||
SettingsData.setTopBarCenterWidgets(defaultCenterWidgets)
|
|
||||||
|
|
||||||
if (!SettingsData.topBarRightWidgets)
|
|
||||||
SettingsData.setTopBarRightWidgets(defaultRightWidgets)
|
|
||||||
|
|
||||||
["left", "center", "right"].forEach(sectionId => {
|
|
||||||
var widgets = []
|
|
||||||
if (sectionId === "left")
|
|
||||||
widgets = SettingsData.topBarLeftWidgets.slice()
|
|
||||||
else if (sectionId === "center")
|
|
||||||
widgets = SettingsData.topBarCenterWidgets.slice()
|
|
||||||
else if (sectionId === "right")
|
|
||||||
widgets = SettingsData.topBarRightWidgets.slice()
|
|
||||||
var updated = false
|
|
||||||
for (var i = 0; i < widgets.length; i++) {
|
|
||||||
var widget = widgets[i]
|
|
||||||
if (typeof widget === "object"
|
|
||||||
&& widget.id === "spacer" && !widget.size) {
|
|
||||||
widgets[i] = Object.assign({}, widget, {
|
|
||||||
"size": 20
|
|
||||||
})
|
|
||||||
updated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updated) {
|
|
||||||
if (sectionId === "left")
|
|
||||||
SettingsData.setTopBarLeftWidgets(widgets)
|
|
||||||
else if (sectionId === "center")
|
|
||||||
SettingsData.setTopBarCenterWidgets(widgets)
|
|
||||||
else if (sectionId === "right")
|
|
||||||
SettingsData.setTopBarRightWidgets(widgets)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "widgets"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Top Bar Widget Management"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 400
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 80
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
border.width: 1
|
|
||||||
border.color: resetArea.containsMouse ? Theme.outline : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "refresh"
|
|
||||||
size: 14
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Reset"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: resetArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
SettingsData.setTopBarLeftWidgets(
|
|
||||||
defaultLeftWidgets)
|
|
||||||
SettingsData.setTopBarCenterWidgets(
|
|
||||||
defaultCenterWidgets)
|
|
||||||
SettingsData.setTopBarRightWidgets(
|
|
||||||
defaultRightWidgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: messageText.contentHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: messageText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outline
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
WidgetsTabSection {
|
|
||||||
width: parent.width
|
|
||||||
title: "Left Section"
|
|
||||||
titleIcon: "format_align_left"
|
|
||||||
sectionId: "left"
|
|
||||||
allWidgets: widgetsTab.baseWidgetDefinitions
|
|
||||||
items: widgetsTab.getItemsForSection("left")
|
|
||||||
onItemEnabledChanged: (sectionId, itemId, enabled) => {
|
|
||||||
widgetsTab.handleItemEnabledChanged(
|
|
||||||
sectionId, itemId, enabled)
|
|
||||||
}
|
|
||||||
onItemOrderChanged: newOrder => {
|
|
||||||
widgetsTab.handleItemOrderChanged(
|
|
||||||
"left", newOrder)
|
|
||||||
}
|
|
||||||
onAddWidget: sectionId => {
|
|
||||||
widgetSelectionPopup.allWidgets
|
|
||||||
= widgetsTab.baseWidgetDefinitions
|
|
||||||
widgetSelectionPopup.targetSection = sectionId
|
|
||||||
widgetSelectionPopup.safeOpen()
|
|
||||||
}
|
|
||||||
onRemoveWidget: (sectionId, widgetIndex) => {
|
|
||||||
widgetsTab.removeWidgetFromSection(
|
|
||||||
sectionId, widgetIndex)
|
|
||||||
}
|
|
||||||
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
|
|
||||||
widgetsTab.handleSpacerSizeChanged(
|
|
||||||
sectionId, itemId, newSize)
|
|
||||||
}
|
|
||||||
onCompactModeChanged: (widgetId, value) => {
|
|
||||||
if (widgetId === "clock") {
|
|
||||||
SettingsData.setClockCompactMode(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "music") {
|
|
||||||
SettingsData.setMediaSize(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "focusedWindow") {
|
|
||||||
SettingsData.setFocusedWindowCompactMode(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "runningApps") {
|
|
||||||
SettingsData.setRunningAppsCompactMode(
|
|
||||||
value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
|
|
||||||
widgetsTab.handleGpuSelectionChanged(
|
|
||||||
sectionId, widgetIndex,
|
|
||||||
selectedIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WidgetsTabSection {
|
|
||||||
width: parent.width
|
|
||||||
title: "Center Section"
|
|
||||||
titleIcon: "format_align_center"
|
|
||||||
sectionId: "center"
|
|
||||||
allWidgets: widgetsTab.baseWidgetDefinitions
|
|
||||||
items: widgetsTab.getItemsForSection("center")
|
|
||||||
onItemEnabledChanged: (sectionId, itemId, enabled) => {
|
|
||||||
widgetsTab.handleItemEnabledChanged(
|
|
||||||
sectionId, itemId, enabled)
|
|
||||||
}
|
|
||||||
onItemOrderChanged: newOrder => {
|
|
||||||
widgetsTab.handleItemOrderChanged(
|
|
||||||
"center", newOrder)
|
|
||||||
}
|
|
||||||
onAddWidget: sectionId => {
|
|
||||||
widgetSelectionPopup.allWidgets
|
|
||||||
= widgetsTab.baseWidgetDefinitions
|
|
||||||
widgetSelectionPopup.targetSection = sectionId
|
|
||||||
widgetSelectionPopup.safeOpen()
|
|
||||||
}
|
|
||||||
onRemoveWidget: (sectionId, widgetIndex) => {
|
|
||||||
widgetsTab.removeWidgetFromSection(
|
|
||||||
sectionId, widgetIndex)
|
|
||||||
}
|
|
||||||
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
|
|
||||||
widgetsTab.handleSpacerSizeChanged(
|
|
||||||
sectionId, itemId, newSize)
|
|
||||||
}
|
|
||||||
onCompactModeChanged: (widgetId, value) => {
|
|
||||||
if (widgetId === "clock") {
|
|
||||||
SettingsData.setClockCompactMode(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "music") {
|
|
||||||
SettingsData.setMediaSize(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "focusedWindow") {
|
|
||||||
SettingsData.setFocusedWindowCompactMode(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "runningApps") {
|
|
||||||
SettingsData.setRunningAppsCompactMode(
|
|
||||||
value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
|
|
||||||
widgetsTab.handleGpuSelectionChanged(
|
|
||||||
sectionId, widgetIndex,
|
|
||||||
selectedIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WidgetsTabSection {
|
|
||||||
width: parent.width
|
|
||||||
title: "Right Section"
|
|
||||||
titleIcon: "format_align_right"
|
|
||||||
sectionId: "right"
|
|
||||||
allWidgets: widgetsTab.baseWidgetDefinitions
|
|
||||||
items: widgetsTab.getItemsForSection("right")
|
|
||||||
onItemEnabledChanged: (sectionId, itemId, enabled) => {
|
|
||||||
widgetsTab.handleItemEnabledChanged(
|
|
||||||
sectionId, itemId, enabled)
|
|
||||||
}
|
|
||||||
onItemOrderChanged: newOrder => {
|
|
||||||
widgetsTab.handleItemOrderChanged(
|
|
||||||
"right", newOrder)
|
|
||||||
}
|
|
||||||
onAddWidget: sectionId => {
|
|
||||||
widgetSelectionPopup.allWidgets
|
|
||||||
= widgetsTab.baseWidgetDefinitions
|
|
||||||
widgetSelectionPopup.targetSection = sectionId
|
|
||||||
widgetSelectionPopup.safeOpen()
|
|
||||||
}
|
|
||||||
onRemoveWidget: (sectionId, widgetIndex) => {
|
|
||||||
widgetsTab.removeWidgetFromSection(
|
|
||||||
sectionId, widgetIndex)
|
|
||||||
}
|
|
||||||
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
|
|
||||||
widgetsTab.handleSpacerSizeChanged(
|
|
||||||
sectionId, itemId, newSize)
|
|
||||||
}
|
|
||||||
onCompactModeChanged: (widgetId, value) => {
|
|
||||||
if (widgetId === "clock") {
|
|
||||||
SettingsData.setClockCompactMode(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "music") {
|
|
||||||
SettingsData.setMediaSize(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "focusedWindow") {
|
|
||||||
SettingsData.setFocusedWindowCompactMode(
|
|
||||||
value)
|
|
||||||
} else if (widgetId === "runningApps") {
|
|
||||||
SettingsData.setRunningAppsCompactMode(
|
|
||||||
value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
|
|
||||||
widgetsTab.handleGpuSelectionChanged(
|
|
||||||
sectionId, widgetIndex,
|
|
||||||
selectedIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: workspaceSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: workspaceSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "view_module"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Workspace Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "Workspace Index Numbers"
|
|
||||||
description: "Show workspace index numbers in the top bar workspace switcher"
|
|
||||||
checked: SettingsData.showWorkspaceIndex
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setShowWorkspaceIndex(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
width: parent.width
|
|
||||||
text: "Workspace Padding"
|
|
||||||
description: "Always show a minimum of 3 workspaces, even if fewer are available"
|
|
||||||
checked: SettingsData.showWorkspacePadding
|
|
||||||
onToggled: checked => {
|
|
||||||
return SettingsData.setShowWorkspacePadding(
|
|
||||||
checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
visible: SettingsData.hasNamedWorkspaces()
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: workspaceIconsSection
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "label"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Named Workspace Icons"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Configure icons for named workspaces. Icons take priority over numbers when both are enabled."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outline
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: SettingsData.getNamedWorkspaces()
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: workspaceIconRow.implicitHeight + Theme.spacingM
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
|
||||||
Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.5)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: workspaceIconRow
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "\"" + modelData + "\""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 150
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIconPicker {
|
|
||||||
id: iconPicker
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
var iconData = SettingsData.getWorkspaceNameIcon(
|
|
||||||
modelData)
|
|
||||||
if (iconData) {
|
|
||||||
setIcon(iconData.value,
|
|
||||||
iconData.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onIconSelected: (iconName, iconType) => {
|
|
||||||
SettingsData.setWorkspaceNameIcon(
|
|
||||||
modelData, {
|
|
||||||
"type": iconType,
|
|
||||||
"value": iconName
|
|
||||||
})
|
|
||||||
setIcon(iconName,
|
|
||||||
iconType)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onWorkspaceIconsUpdated() {
|
|
||||||
var iconData = SettingsData.getWorkspaceNameIcon(
|
|
||||||
modelData)
|
|
||||||
if (iconData) {
|
|
||||||
iconPicker.setIcon(
|
|
||||||
iconData.value,
|
|
||||||
iconData.type)
|
|
||||||
} else {
|
|
||||||
iconPicker.setIcon("", "icon")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer
|
|
||||||
border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "close"
|
|
||||||
size: 16
|
|
||||||
color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clearMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
SettingsData.removeWorkspaceNameIcon(
|
|
||||||
modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankWidgetSelectionPopup {
|
|
||||||
id: widgetSelectionPopup
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
onWidgetSelected: (widgetId, targetSection) => {
|
|
||||||
widgetsTab.addWidgetToSection(widgetId,
|
|
||||||
targetSection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,761 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var items: []
|
|
||||||
property var allWidgets: []
|
|
||||||
property string title: ""
|
|
||||||
property string titleIcon: "widgets"
|
|
||||||
property string sectionId: ""
|
|
||||||
|
|
||||||
signal itemEnabledChanged(string sectionId, string itemId, bool enabled)
|
|
||||||
signal itemOrderChanged(var newOrder)
|
|
||||||
signal addWidget(string sectionId)
|
|
||||||
signal removeWidget(string sectionId, int widgetIndex)
|
|
||||||
signal spacerSizeChanged(string sectionId, int widgetIndex, int newSize)
|
|
||||||
signal compactModeChanged(string widgetId, var value)
|
|
||||||
signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex)
|
|
||||||
signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value)
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: implicitHeight
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.titleIcon
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.title
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 60
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: itemsList
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.items
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateItem
|
|
||||||
|
|
||||||
property bool held: dragArea.pressed
|
|
||||||
property real originalY: y
|
|
||||||
|
|
||||||
width: itemsList.width
|
|
||||||
height: 70
|
|
||||||
z: held ? 2 : 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: itemBackground
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
|
||||||
Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.8)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "drag_indicator"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.outline
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM + 8
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: modelData.icon
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: modelData.enabled ? Theme.primary : Theme.outline
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM * 2 + 40
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize
|
|
||||||
anchors.right: actionButtons.left
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.text
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: modelData.enabled ? Theme.surfaceText : Theme.outline
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.description
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: modelData.enabled ? Theme.outline : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.6)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: actionButtons
|
|
||||||
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 120
|
|
||||||
height: 32
|
|
||||||
visible: modelData.id === "gpuTemp"
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
id: gpuDropdown
|
|
||||||
anchors.fill: parent
|
|
||||||
currentValue: {
|
|
||||||
var selectedIndex = modelData.selectedGpuIndex
|
|
||||||
!== undefined ? modelData.selectedGpuIndex : 0
|
|
||||||
if (DgopService.availableGpus
|
|
||||||
&& DgopService.availableGpus.length > selectedIndex
|
|
||||||
&& selectedIndex >= 0) {
|
|
||||||
var gpu = DgopService.availableGpus[selectedIndex]
|
|
||||||
return gpu.driver.toUpperCase()
|
|
||||||
}
|
|
||||||
return DgopService.availableGpus
|
|
||||||
&& DgopService.availableGpus.length
|
|
||||||
> 0 ? DgopService.availableGpus[0].driver.toUpperCase(
|
|
||||||
) : ""
|
|
||||||
}
|
|
||||||
options: {
|
|
||||||
var gpuOptions = []
|
|
||||||
if (DgopService.availableGpus
|
|
||||||
&& DgopService.availableGpus.length > 0) {
|
|
||||||
for (var i = 0; i < DgopService.availableGpus.length; i++) {
|
|
||||||
var gpu = DgopService.availableGpus[i]
|
|
||||||
gpuOptions.push(
|
|
||||||
gpu.driver.toUpperCase(
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gpuOptions
|
|
||||||
}
|
|
||||||
onValueChanged: value => {
|
|
||||||
var gpuIndex = options.indexOf(
|
|
||||||
value)
|
|
||||||
if (gpuIndex >= 0) {
|
|
||||||
root.gpuSelectionChanged(
|
|
||||||
root.sectionId,
|
|
||||||
index, gpuIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
visible: (modelData.warning !== undefined
|
|
||||||
&& modelData.warning !== "")
|
|
||||||
&& (modelData.id === "cpuUsage"
|
|
||||||
|| modelData.id === "memUsage"
|
|
||||||
|| modelData.id === "cpuTemp"
|
|
||||||
|| modelData.id === "gpuTemp")
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "warning"
|
|
||||||
size: 20
|
|
||||||
color: Theme.error
|
|
||||||
anchors.centerIn: parent
|
|
||||||
opacity: warningArea.containsMouse ? 1.0 : 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: warningArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: warningTooltip
|
|
||||||
|
|
||||||
property string warningText: (modelData.warning !== undefined
|
|
||||||
&& modelData.warning
|
|
||||||
!== "") ? modelData.warning : ""
|
|
||||||
|
|
||||||
width: Math.min(
|
|
||||||
250,
|
|
||||||
warningTooltipText.implicitWidth) + Theme.spacingM * 2
|
|
||||||
height: warningTooltipText.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
visible: warningArea.containsMouse
|
|
||||||
&& warningText !== ""
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
x: -width - Theme.spacingS
|
|
||||||
y: (parent.height - height) / 2
|
|
||||||
z: 100
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: warningTooltipText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
text: warningTooltip.warningText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: Math.min(250, implicitWidth)
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: modelData.id === "clock"
|
|
||||||
|| modelData.id === "music"
|
|
||||||
|| modelData.id === "focusedWindow"
|
|
||||||
|| modelData.id === "runningApps"
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: smallSizeButton
|
|
||||||
buttonSize: 28
|
|
||||||
visible: modelData.id === "music"
|
|
||||||
iconName: "photo_size_select_small"
|
|
||||||
iconSize: 16
|
|
||||||
iconColor: SettingsData.mediaSize
|
|
||||||
=== 0 ? Theme.primary : Theme.outline
|
|
||||||
onClicked: {
|
|
||||||
root.compactModeChanged("music", 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: mediumSizeButton
|
|
||||||
buttonSize: 28
|
|
||||||
visible: modelData.id === "music"
|
|
||||||
iconName: "photo_size_select_actual"
|
|
||||||
iconSize: 16
|
|
||||||
iconColor: SettingsData.mediaSize
|
|
||||||
=== 1 ? Theme.primary : Theme.outline
|
|
||||||
onClicked: {
|
|
||||||
root.compactModeChanged("music", 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: largeSizeButton
|
|
||||||
buttonSize: 28
|
|
||||||
visible: modelData.id === "music"
|
|
||||||
iconName: "photo_size_select_large"
|
|
||||||
iconSize: 16
|
|
||||||
iconColor: SettingsData.mediaSize
|
|
||||||
=== 2 ? Theme.primary : Theme.outline
|
|
||||||
onClicked: {
|
|
||||||
root.compactModeChanged("music", 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: compactModeButton
|
|
||||||
buttonSize: 28
|
|
||||||
visible: modelData.id === "clock"
|
|
||||||
|| modelData.id === "focusedWindow"
|
|
||||||
|| modelData.id === "runningApps"
|
|
||||||
iconName: {
|
|
||||||
if (modelData.id === "clock")
|
|
||||||
return SettingsData.clockCompactMode ? "zoom_out" : "zoom_in"
|
|
||||||
if (modelData.id === "focusedWindow")
|
|
||||||
return SettingsData.focusedWindowCompactMode ? "zoom_out" : "zoom_in"
|
|
||||||
if (modelData.id === "runningApps")
|
|
||||||
return SettingsData.runningAppsCompactMode ? "zoom_out" : "zoom_in"
|
|
||||||
return "zoom_in"
|
|
||||||
}
|
|
||||||
iconSize: 16
|
|
||||||
iconColor: {
|
|
||||||
if (modelData.id === "clock")
|
|
||||||
return SettingsData.clockCompactMode ? Theme.primary : Theme.outline
|
|
||||||
if (modelData.id === "focusedWindow")
|
|
||||||
return SettingsData.focusedWindowCompactMode ? Theme.primary : Theme.outline
|
|
||||||
if (modelData.id === "runningApps")
|
|
||||||
return SettingsData.runningAppsCompactMode ? Theme.primary : Theme.outline
|
|
||||||
return Theme.outline
|
|
||||||
}
|
|
||||||
onClicked: {
|
|
||||||
if (modelData.id === "clock") {
|
|
||||||
root.compactModeChanged(
|
|
||||||
"clock",
|
|
||||||
!SettingsData.clockCompactMode)
|
|
||||||
} else if (modelData.id === "focusedWindow") {
|
|
||||||
root.compactModeChanged(
|
|
||||||
"focusedWindow",
|
|
||||||
!SettingsData.focusedWindowCompactMode)
|
|
||||||
} else if (modelData.id === "runningApps") {
|
|
||||||
root.compactModeChanged(
|
|
||||||
"runningApps",
|
|
||||||
!SettingsData.runningAppsCompactMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: compactModeTooltip
|
|
||||||
width: tooltipText.contentWidth + Theme.spacingM * 2
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
visible: false
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
x: -width - Theme.spacingS
|
|
||||||
y: (parent.height - height) / 2
|
|
||||||
z: 100
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Compact Mode"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
visible: modelData.id === "controlCenterButton"
|
|
||||||
buttonSize: 32
|
|
||||||
iconName: "more_vert"
|
|
||||||
iconSize: 18
|
|
||||||
iconColor: Theme.outline
|
|
||||||
onClicked: {
|
|
||||||
console.log("Control Center three-dot button clicked for widget:", modelData.id)
|
|
||||||
controlCenterContextMenu.widgetData = modelData
|
|
||||||
controlCenterContextMenu.sectionId = root.sectionId
|
|
||||||
controlCenterContextMenu.widgetIndex = index
|
|
||||||
// Position relative to the action buttons row, not the specific button
|
|
||||||
var parentPos = parent.mapToItem(root, 0, 0)
|
|
||||||
controlCenterContextMenu.x = parentPos.x - 210 // Position to the left with margin
|
|
||||||
controlCenterContextMenu.y = parentPos.y - 10 // Slightly above
|
|
||||||
controlCenterContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
visible: modelData.id !== "spacer"
|
|
||||||
buttonSize: 32
|
|
||||||
iconName: modelData.enabled ? "visibility" : "visibility_off"
|
|
||||||
iconSize: 18
|
|
||||||
iconColor: modelData.enabled ? Theme.primary : Theme.outline
|
|
||||||
onClicked: {
|
|
||||||
root.itemEnabledChanged(root.sectionId,
|
|
||||||
modelData.id,
|
|
||||||
!modelData.enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
visible: modelData.id === "spacer"
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 24
|
|
||||||
iconName: "remove"
|
|
||||||
iconSize: 14
|
|
||||||
iconColor: Theme.outline
|
|
||||||
onClicked: {
|
|
||||||
var currentSize = modelData.size || 20
|
|
||||||
var newSize = Math.max(5, currentSize - 5)
|
|
||||||
root.spacerSizeChanged(root.sectionId,
|
|
||||||
index,
|
|
||||||
newSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (modelData.size || 20).toString()
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 24
|
|
||||||
iconName: "add"
|
|
||||||
iconSize: 14
|
|
||||||
iconColor: Theme.outline
|
|
||||||
onClicked: {
|
|
||||||
var currentSize = modelData.size || 20
|
|
||||||
var newSize = Math.min(5000,
|
|
||||||
currentSize + 5)
|
|
||||||
root.spacerSizeChanged(root.sectionId,
|
|
||||||
index,
|
|
||||||
newSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 32
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: 18
|
|
||||||
iconColor: Theme.error
|
|
||||||
onClicked: {
|
|
||||||
root.removeWidget(root.sectionId, index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dragArea
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: 60
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.SizeVerCursor
|
|
||||||
drag.target: held ? delegateItem : undefined
|
|
||||||
drag.axis: Drag.YAxis
|
|
||||||
drag.minimumY: -delegateItem.height
|
|
||||||
drag.maximumY: itemsList.height
|
|
||||||
preventStealing: true
|
|
||||||
onPressed: {
|
|
||||||
delegateItem.z = 2
|
|
||||||
delegateItem.originalY = delegateItem.y
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
delegateItem.z = 1
|
|
||||||
if (drag.active) {
|
|
||||||
var newIndex = Math.round(
|
|
||||||
delegateItem.y / (delegateItem.height
|
|
||||||
+ itemsList.spacing))
|
|
||||||
newIndex = Math.max(
|
|
||||||
0, Math.min(newIndex,
|
|
||||||
root.items.length - 1))
|
|
||||||
if (newIndex !== index) {
|
|
||||||
var newItems = root.items.slice()
|
|
||||||
var draggedItem = newItems.splice(index,
|
|
||||||
1)[0]
|
|
||||||
newItems.splice(newIndex, 0, draggedItem)
|
|
||||||
root.itemOrderChanged(newItems.map(item => {
|
|
||||||
return ({
|
|
||||||
"id": item.id,
|
|
||||||
"enabled": item.enabled,
|
|
||||||
"size": item.size
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delegateItem.x = 0
|
|
||||||
delegateItem.y = delegateItem.originalY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
enabled: !dragArea.held && !dragArea.drag.active
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 200
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: addButtonArea.containsMouse ? Theme.primaryContainer : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Add Widget"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: addButtonArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.addWidget(root.sectionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
id: controlCenterContextMenu
|
|
||||||
|
|
||||||
property var widgetData: null
|
|
||||||
property string sectionId: ""
|
|
||||||
property int widgetIndex: -1
|
|
||||||
|
|
||||||
|
|
||||||
width: 200
|
|
||||||
height: 120
|
|
||||||
padding: 0
|
|
||||||
modal: true
|
|
||||||
focus: true
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
|
|
||||||
onOpened: {
|
|
||||||
console.log("Control Center context menu opened")
|
|
||||||
}
|
|
||||||
|
|
||||||
onClosed: {
|
|
||||||
console.log("Control Center context menu closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: Item {
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "lan"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Network Icon"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: networkToggle
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 40
|
|
||||||
height: 20
|
|
||||||
checked: SettingsData.controlCenterShowNetworkIcon
|
|
||||||
onToggled: {
|
|
||||||
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showNetworkIcon", toggled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: networkToggleArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
networkToggle.checked = !networkToggle.checked
|
|
||||||
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showNetworkIcon", networkToggle.checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: bluetoothToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bluetooth"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Bluetooth Icon"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: bluetoothToggle
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 40
|
|
||||||
height: 20
|
|
||||||
checked: SettingsData.controlCenterShowBluetoothIcon
|
|
||||||
onToggled: {
|
|
||||||
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showBluetoothIcon", toggled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: bluetoothToggleArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
bluetoothToggle.checked = !bluetoothToggle.checked
|
|
||||||
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showBluetoothIcon", bluetoothToggle.checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: audioToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "volume_up"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Audio Icon"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: audioToggle
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 40
|
|
||||||
height: 20
|
|
||||||
checked: SettingsData.controlCenterShowAudioIcon
|
|
||||||
onToggled: {
|
|
||||||
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showAudioIcon", toggled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: audioToggleArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
audioToggle.checked = !audioToggle.checked
|
|
||||||
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showAudioIcon", audioToggle.checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +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,211 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Services.UPower
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: battery
|
|
||||||
|
|
||||||
property bool batteryPopupVisible: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal toggleBatteryPopup
|
|
||||||
|
|
||||||
width: batteryContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = batteryArea.containsMouse
|
|
||||||
|| batteryPopupVisible ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
visible: true
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: batteryContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: SettingsData.topBarNoBackground ? 1 : 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (!BatteryService.batteryAvailable)
|
|
||||||
return "power"
|
|
||||||
|
|
||||||
if (BatteryService.isCharging) {
|
|
||||||
if (BatteryService.batteryLevel >= 90)
|
|
||||||
return "battery_charging_full"
|
|
||||||
if (BatteryService.batteryLevel >= 80)
|
|
||||||
return "battery_charging_90"
|
|
||||||
if (BatteryService.batteryLevel >= 60)
|
|
||||||
return "battery_charging_80"
|
|
||||||
if (BatteryService.batteryLevel >= 50)
|
|
||||||
return "battery_charging_60"
|
|
||||||
if (BatteryService.batteryLevel >= 30)
|
|
||||||
return "battery_charging_50"
|
|
||||||
if (BatteryService.batteryLevel >= 20)
|
|
||||||
return "battery_charging_30"
|
|
||||||
return "battery_charging_20"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if plugged in but not charging (like at 80% charge limit)
|
|
||||||
if (BatteryService.isPluggedIn) {
|
|
||||||
if (BatteryService.batteryLevel >= 90)
|
|
||||||
return "battery_charging_full"
|
|
||||||
if (BatteryService.batteryLevel >= 80)
|
|
||||||
return "battery_charging_90"
|
|
||||||
if (BatteryService.batteryLevel >= 60)
|
|
||||||
return "battery_charging_80"
|
|
||||||
if (BatteryService.batteryLevel >= 50)
|
|
||||||
return "battery_charging_60"
|
|
||||||
if (BatteryService.batteryLevel >= 30)
|
|
||||||
return "battery_charging_50"
|
|
||||||
if (BatteryService.batteryLevel >= 20)
|
|
||||||
return "battery_charging_30"
|
|
||||||
return "battery_charging_20"
|
|
||||||
}
|
|
||||||
|
|
||||||
// On battery power
|
|
||||||
if (BatteryService.batteryLevel >= 95)
|
|
||||||
return "battery_full"
|
|
||||||
if (BatteryService.batteryLevel >= 85)
|
|
||||||
return "battery_6_bar"
|
|
||||||
if (BatteryService.batteryLevel >= 70)
|
|
||||||
return "battery_5_bar"
|
|
||||||
if (BatteryService.batteryLevel >= 55)
|
|
||||||
return "battery_4_bar"
|
|
||||||
if (BatteryService.batteryLevel >= 40)
|
|
||||||
return "battery_3_bar"
|
|
||||||
if (BatteryService.batteryLevel >= 25)
|
|
||||||
return "battery_2_bar"
|
|
||||||
return "battery_1_bar"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: {
|
|
||||||
if (!BatteryService.batteryAvailable)
|
|
||||||
return Theme.surfaceText
|
|
||||||
|
|
||||||
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
if (BatteryService.isCharging || BatteryService.isPluggedIn)
|
|
||||||
return Theme.primary
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BatteryService.batteryLevel + "%"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: {
|
|
||||||
if (!BatteryService.batteryAvailable)
|
|
||||||
return Theme.surfaceText
|
|
||||||
|
|
||||||
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
if (BatteryService.isCharging)
|
|
||||||
return Theme.primary
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: BatteryService.batteryAvailable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: batteryArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
toggleBatteryPopup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: batteryTooltip
|
|
||||||
|
|
||||||
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
visible: batteryArea.containsMouse && !batteryPopupVisible
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
opacity: batteryArea.containsMouse ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipText
|
|
||||||
|
|
||||||
text: {
|
|
||||||
if (!BatteryService.batteryAvailable) {
|
|
||||||
if (typeof PowerProfiles === "undefined")
|
|
||||||
return "Power Management"
|
|
||||||
|
|
||||||
switch (PowerProfiles.profile) {
|
|
||||||
case PowerProfile.PowerSaver:
|
|
||||||
return "Power Profile: Power Saver"
|
|
||||||
case PowerProfile.Performance:
|
|
||||||
return "Power Profile: Performance"
|
|
||||||
default:
|
|
||||||
return "Power Profile: Balanced"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let status = BatteryService.batteryStatus
|
|
||||||
let level = BatteryService.batteryLevel + "%"
|
|
||||||
let time = BatteryService.formatTimeRemaining()
|
|
||||||
if (time !== "Unknown")
|
|
||||||
return status + " • " + level + " • " + time
|
|
||||||
else
|
|
||||||
return status + " • " + level
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,546 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.UPower
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankPopout {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string triggerSection: "right"
|
|
||||||
property var triggerScreen: null
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
triggerX = x
|
|
||||||
triggerY = y
|
|
||||||
triggerWidth = width
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActiveProfile(profile) {
|
|
||||||
if (typeof PowerProfiles === "undefined")
|
|
||||||
return false
|
|
||||||
|
|
||||||
return PowerProfiles.profile === profile
|
|
||||||
}
|
|
||||||
|
|
||||||
function setProfile(profile) {
|
|
||||||
if (typeof PowerProfiles === "undefined") {
|
|
||||||
ToastService.showError("power-profiles-daemon not available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PowerProfiles.profile = profile
|
|
||||||
if (PowerProfiles.profile !== profile)
|
|
||||||
ToastService.showError("Failed to set power profile")
|
|
||||||
}
|
|
||||||
|
|
||||||
popupWidth: 400
|
|
||||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
|
|
||||||
triggerX: Screen.width - 380 - Theme.spacingL
|
|
||||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS
|
|
||||||
triggerWidth: 70
|
|
||||||
positioning: "center"
|
|
||||||
WlrLayershell.namespace: "quickshell-battery"
|
|
||||||
screen: triggerScreen
|
|
||||||
shouldBeVisible: false
|
|
||||||
visible: shouldBeVisible
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Rectangle {
|
|
||||||
id: batteryContent
|
|
||||||
|
|
||||||
implicitHeight: contentColumn.height + Theme.spacingL * 2
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
antialiasing: true
|
|
||||||
smooth: true
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
Qt.callLater(function () {
|
|
||||||
batteryContent.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -3
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 3
|
|
||||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
|
||||||
border.width: 1
|
|
||||||
z: -3
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -2
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 2
|
|
||||||
border.color: Theme.shadowMedium
|
|
||||||
border.width: 1
|
|
||||||
z: -2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 1
|
|
||||||
radius: parent.radius
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BatteryService.batteryAvailable ? "Battery Information" : "Power Management"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent"
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "close"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: closeBatteryArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: closeBatteryArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha() * 0.4)
|
|
||||||
border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Theme.outlineMedium)
|
|
||||||
border.width: BatteryService.isCharging
|
|
||||||
|| BatteryService.isLowBattery ? 2 : 1
|
|
||||||
visible: BatteryService.batteryAvailable
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (!BatteryService.batteryAvailable)
|
|
||||||
return "power"
|
|
||||||
|
|
||||||
// Check if plugged in but not charging (like at 80% charge limit)
|
|
||||||
if (!BatteryService.isCharging
|
|
||||||
&& BatteryService.isPluggedIn) {
|
|
||||||
if (BatteryService.batteryLevel >= 90)
|
|
||||||
return "battery_charging_full"
|
|
||||||
if (BatteryService.batteryLevel >= 80)
|
|
||||||
return "battery_charging_90"
|
|
||||||
if (BatteryService.batteryLevel >= 60)
|
|
||||||
return "battery_charging_80"
|
|
||||||
if (BatteryService.batteryLevel >= 50)
|
|
||||||
return "battery_charging_60"
|
|
||||||
if (BatteryService.batteryLevel >= 30)
|
|
||||||
return "battery_charging_50"
|
|
||||||
if (BatteryService.batteryLevel >= 20)
|
|
||||||
return "battery_charging_30"
|
|
||||||
return "battery_charging_20"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BatteryService.isCharging) {
|
|
||||||
if (BatteryService.batteryLevel >= 90)
|
|
||||||
return "battery_charging_full"
|
|
||||||
if (BatteryService.batteryLevel >= 80)
|
|
||||||
return "battery_charging_90"
|
|
||||||
if (BatteryService.batteryLevel >= 60)
|
|
||||||
return "battery_charging_80"
|
|
||||||
if (BatteryService.batteryLevel >= 50)
|
|
||||||
return "battery_charging_60"
|
|
||||||
if (BatteryService.batteryLevel >= 30)
|
|
||||||
return "battery_charging_50"
|
|
||||||
if (BatteryService.batteryLevel >= 20)
|
|
||||||
return "battery_charging_30"
|
|
||||||
return "battery_charging_20"
|
|
||||||
} else {
|
|
||||||
if (BatteryService.batteryLevel >= 95)
|
|
||||||
return "battery_full"
|
|
||||||
if (BatteryService.batteryLevel >= 85)
|
|
||||||
return "battery_6_bar"
|
|
||||||
if (BatteryService.batteryLevel >= 70)
|
|
||||||
return "battery_5_bar"
|
|
||||||
if (BatteryService.batteryLevel >= 55)
|
|
||||||
return "battery_4_bar"
|
|
||||||
if (BatteryService.batteryLevel >= 40)
|
|
||||||
return "battery_3_bar"
|
|
||||||
if (BatteryService.batteryLevel >= 25)
|
|
||||||
return "battery_2_bar"
|
|
||||||
return "battery_1_bar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: {
|
|
||||||
if (BatteryService.isLowBattery
|
|
||||||
&& !BatteryService.isCharging)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
if (BatteryService.isCharging
|
|
||||||
|| BatteryService.isPluggedIn)
|
|
||||||
return Theme.primary
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BatteryService.batteryLevel + "%"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: {
|
|
||||||
if (BatteryService.isLowBattery
|
|
||||||
&& !BatteryService.isCharging)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
if (BatteryService.isCharging)
|
|
||||||
return Theme.primary
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BatteryService.batteryStatus
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: {
|
|
||||||
if (BatteryService.isLowBattery
|
|
||||||
&& !BatteryService.isCharging)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
if (BatteryService.isCharging)
|
|
||||||
return Theme.primary
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
let time = BatteryService.formatTimeRemaining()
|
|
||||||
if (time !== "Unknown")
|
|
||||||
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha() * 0.4)
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
visible: !BatteryService.batteryAvailable
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power"
|
|
||||||
size: 36
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No Battery Detected"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Power profile management is available"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: BatteryService.batteryAvailable
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Battery Details"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
width: (parent.width - Theme.spacingXL) / 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Health"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BatteryService.batteryHealth
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: {
|
|
||||||
if (BatteryService.batteryHealth === "N/A")
|
|
||||||
return Theme.surfaceText
|
|
||||||
|
|
||||||
var healthNum = parseInt(
|
|
||||||
BatteryService.batteryHealth)
|
|
||||||
return healthNum < 80 ? Theme.error : Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
width: (parent.width - Theme.spacingXL) / 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Capacity"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BatteryService.batteryCapacity
|
|
||||||
> 0 ? BatteryService.batteryCapacity.toFixed(
|
|
||||||
1) + " Wh" : "Unknown"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: true
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Power Profile"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: profileArea.containsMouse ? Theme.primaryHoverLight : (root.isActiveProfile(modelData) ? Theme.primaryPressed : Theme.surfaceLight)
|
|
||||||
border.color: root.isActiveProfile(
|
|
||||||
modelData) ? Theme.primary : Theme.outlineLight
|
|
||||||
border.width: root.isActiveProfile(
|
|
||||||
modelData) ? 2 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: Theme.getPowerProfileIcon(
|
|
||||||
modelData)
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: root.isActiveProfile(
|
|
||||||
modelData) ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: Theme.getPowerProfileLabel(
|
|
||||||
modelData)
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: root.isActiveProfile(
|
|
||||||
modelData) ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: root.isActiveProfile(
|
|
||||||
modelData) ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: Theme.getPowerProfileDescription(
|
|
||||||
modelData)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: profileArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.setProfile(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.errorHover
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
border.width: 1
|
|
||||||
visible: (typeof PowerProfiles !== "undefined")
|
|
||||||
&& PowerProfiles.degradationReason
|
|
||||||
!== PerformanceDegradationReason.None
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "warning"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.error
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Power Profile Degradation"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.error
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (typeof PowerProfiles
|
|
||||||
!== "undefined") ? PerformanceDegradationReason.toString(
|
|
||||||
PowerProfiles.degradationReason) : ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.error.r, Theme.error.g,
|
|
||||||
Theme.error.b, 0.8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property date currentDate: new Date()
|
|
||||||
property bool compactMode: false
|
|
||||||
property string section: "center"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
|
|
||||||
signal clockClicked
|
|
||||||
|
|
||||||
width: clockRow.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = clockMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
root.currentDate = systemClock.date
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: clockRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (SettingsData.use24HourClock) {
|
|
||||||
return root.currentDate.toLocaleTimeString(Qt.locale(), Locale.ShortFormat)
|
|
||||||
} else {
|
|
||||||
return root.currentDate.toLocaleTimeString(Qt.locale(), "h:mm AP")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium - 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "•"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outlineButton
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !SettingsData.clockCompactMode
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
|
|
||||||
return root.currentDate.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat)
|
|
||||||
}
|
|
||||||
return root.currentDate.toLocaleDateString(Qt.locale(), "ddd d")
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium - 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !SettingsData.clockCompactMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemClock {
|
|
||||||
id: systemClock
|
|
||||||
|
|
||||||
precision: SystemClock.Seconds
|
|
||||||
onDateChanged: root.currentDate = systemClock.date
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clockMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
root.clockClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property var widgetData: null
|
|
||||||
|
|
||||||
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
|
|
||||||
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
|
|
||||||
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked
|
|
||||||
|
|
||||||
width: controlIndicators.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = controlCenterArea.containsMouse
|
|
||||||
|| root.isActive ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: controlIndicators
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: networkIcon
|
|
||||||
name: {
|
|
||||||
if (NetworkService.wifiToggling)
|
|
||||||
return "sync"
|
|
||||||
if (NetworkService.networkStatus === "ethernet")
|
|
||||||
return "lan"
|
|
||||||
return NetworkService.wifiSignalIcon
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (NetworkService.wifiToggling) return Theme.primary
|
|
||||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showNetworkIcon
|
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
|
||||||
running: NetworkService.wifiToggling
|
|
||||||
loops: Animation.Infinite
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: bluetoothIcon
|
|
||||||
name: "bluetooth"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: BluetoothService.enabled ? Theme.primary : Theme.outlineButton
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: audioIcon.implicitWidth + 4
|
|
||||||
height: audioIcon.implicitHeight + 4
|
|
||||||
color: "transparent"
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showAudioIcon
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: audioIcon
|
|
||||||
|
|
||||||
name: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
if (AudioService.sink.audio.muted
|
|
||||||
|| AudioService.sink.audio.volume === 0)
|
|
||||||
return "volume_off"
|
|
||||||
else if (AudioService.sink.audio.volume * 100 < 33)
|
|
||||||
return "volume_down"
|
|
||||||
else
|
|
||||||
return "volume_up"
|
|
||||||
}
|
|
||||||
return "volume_up"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: audioWheelArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
onWheel: function (wheelEvent) {
|
|
||||||
let delta = wheelEvent.angleDelta.y
|
|
||||||
let currentVolume = (AudioService.sink
|
|
||||||
&& AudioService.sink.audio
|
|
||||||
&& AudioService.sink.audio.volume * 100)
|
|
||||||
|| 0
|
|
||||||
let newVolume
|
|
||||||
if (delta > 0)
|
|
||||||
newVolume = Math.min(100, currentVolume + 5)
|
|
||||||
else
|
|
||||||
newVolume = Math.max(0, currentVolume - 5)
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.muted = false
|
|
||||||
AudioService.sink.audio.volume = newVolume / 100
|
|
||||||
AudioService.volumeChanged()
|
|
||||||
}
|
|
||||||
wheelEvent.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "mic"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: false // TODO: Add mic detection
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback settings icon when all other icons are hidden
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: controlCenterArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
|
|
||||||
root.clicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showPercentage: true
|
|
||||||
property bool showIcon: true
|
|
||||||
property var toggleProcessList
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: cpuContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = cpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["cpu"])
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["cpu"])
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cpuArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
DgopService.setSortBy("cpu")
|
|
||||||
if (root.toggleProcessList)
|
|
||||||
root.toggleProcessList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: cpuContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "memory"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (DgopService.cpuUsage > 80)
|
|
||||||
return Theme.tempDanger
|
|
||||||
|
|
||||||
if (DgopService.cpuUsage > 60)
|
|
||||||
return Theme.tempWarning
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.cpuUsage === undefined
|
|
||||||
|| DgopService.cpuUsage === null
|
|
||||||
|| DgopService.cpuUsage === 0) {
|
|
||||||
return "--%"
|
|
||||||
}
|
|
||||||
return DgopService.cpuUsage.toFixed(0) + "%"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showPercentage: true
|
|
||||||
property bool showIcon: true
|
|
||||||
property var toggleProcessList
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: cpuTempContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = cpuTempArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["cpu"])
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["cpu"])
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cpuTempArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
DgopService.setSortBy("cpu")
|
|
||||||
if (root.toggleProcessList)
|
|
||||||
root.toggleProcessList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: cpuTempContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "memory"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (DgopService.cpuTemperature > 85)
|
|
||||||
return Theme.tempDanger
|
|
||||||
|
|
||||||
if (DgopService.cpuTemperature > 69)
|
|
||||||
return Theme.tempWarning
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.cpuTemperature === undefined
|
|
||||||
|| DgopService.cpuTemperature === null
|
|
||||||
|| DgopService.cpuTemperature < 0) {
|
|
||||||
return "--°"
|
|
||||||
}
|
|
||||||
return Math.round(DgopService.cpuTemperature) + "°"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool compactMode: SettingsData.focusedWindowCompactMode
|
|
||||||
property int availableWidth: 400
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
readonly property int baseWidth: contentRow.implicitWidth + horizontalPadding * 2
|
|
||||||
readonly property int maxNormalWidth: 456
|
|
||||||
readonly property int maxCompactWidth: 288
|
|
||||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
|
||||||
|
|
||||||
width: compactMode ? Math.min(baseWidth,
|
|
||||||
maxCompactWidth) : Math.min(baseWidth,
|
|
||||||
maxNormalWidth)
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (!activeWindow || !activeWindow.title)
|
|
||||||
return "transparent"
|
|
||||||
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
clip: true
|
|
||||||
visible: activeWindow && activeWindow.title
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: appText
|
|
||||||
|
|
||||||
text: {
|
|
||||||
if (!activeWindow || !activeWindow.appId)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(activeWindow.appId)
|
|
||||||
return desktopEntry
|
|
||||||
&& desktopEntry.name ? desktopEntry.name : activeWindow.appId
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
width: Math.min(implicitWidth, compactMode ? 80 : 180)
|
|
||||||
visible: !compactMode && text.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "•"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.outlineButton
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !compactMode && appText.text && titleText.text
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: titleText
|
|
||||||
|
|
||||||
text: {
|
|
||||||
var title = activeWindow && activeWindow.title ? activeWindow.title : ""
|
|
||||||
var appName = appText.text
|
|
||||||
|
|
||||||
if (!title || !appName)
|
|
||||||
return title
|
|
||||||
|
|
||||||
// Remove app name from end of title if it exists there
|
|
||||||
if (title.endsWith(" - " + appName)) {
|
|
||||||
return title.substring(
|
|
||||||
0, title.length - (" - " + appName).length)
|
|
||||||
}
|
|
||||||
if (title.endsWith(appName)) {
|
|
||||||
return title.substring(
|
|
||||||
0, title.length - appName.length).replace(
|
|
||||||
/ - $/, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return title
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
width: Math.min(implicitWidth, compactMode ? 280 : 250)
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showPercentage: true
|
|
||||||
property bool showIcon: true
|
|
||||||
property var toggleProcessList
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property var widgetData: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex
|
|
||||||
!== undefined) ? widgetData.selectedGpuIndex : 0
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onWidgetDataChanged() {
|
|
||||||
// Force property re-evaluation by triggering change detection
|
|
||||||
root.selectedGpuIndex = Qt.binding(() => {
|
|
||||||
return (root.widgetData
|
|
||||||
&& root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: gpuTempContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = gpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["gpu"])
|
|
||||||
console.log("GpuTemperature widget - pciId:",
|
|
||||||
widgetData ? widgetData.pciId : "no widgetData",
|
|
||||||
"selectedGpuIndex:",
|
|
||||||
widgetData ? widgetData.selectedGpuIndex : "no widgetData")
|
|
||||||
// Add this widget's PCI ID to the service
|
|
||||||
if (widgetData && widgetData.pciId) {
|
|
||||||
console.log("Adding GPU PCI ID to service:", widgetData.pciId)
|
|
||||||
DgopService.addGpuPciId(widgetData.pciId)
|
|
||||||
} else {
|
|
||||||
console.log("No PCI ID in widget data, starting auto-detection")
|
|
||||||
// No PCI ID saved, auto-detect and save the first GPU
|
|
||||||
autoSaveTimer.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["gpu"])
|
|
||||||
// Remove this widget's PCI ID from the service
|
|
||||||
if (widgetData && widgetData.pciId) {
|
|
||||||
DgopService.removeGpuPciId(widgetData.pciId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property real displayTemp: {
|
|
||||||
if (!DgopService.availableGpus
|
|
||||||
|| DgopService.availableGpus.length === 0)
|
|
||||||
return 0
|
|
||||||
if (selectedGpuIndex >= 0
|
|
||||||
&& selectedGpuIndex < DgopService.availableGpus.length) {
|
|
||||||
return DgopService.availableGpus[selectedGpuIndex].temperature || 0
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gpuArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
DgopService.setSortBy("cpu")
|
|
||||||
if (root.toggleProcessList)
|
|
||||||
root.toggleProcessList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: gpuTempContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "auto_awesome_mosaic"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (root.displayTemp > 80)
|
|
||||||
return Theme.tempDanger
|
|
||||||
|
|
||||||
if (root.displayTemp > 65)
|
|
||||||
return Theme.tempWarning
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (root.displayTemp === undefined || root.displayTemp === null
|
|
||||||
|| root.displayTemp === 0) {
|
|
||||||
return "--°"
|
|
||||||
}
|
|
||||||
return Math.round(root.displayTemp) + "°"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: autoSaveTimer
|
|
||||||
interval: 100
|
|
||||||
running: false
|
|
||||||
onTriggered: {
|
|
||||||
if (DgopService.availableGpus
|
|
||||||
&& DgopService.availableGpus.length > 0) {
|
|
||||||
const firstGpu = DgopService.availableGpus[0]
|
|
||||||
if (firstGpu && firstGpu.pciId) {
|
|
||||||
// Save the first GPU's PCI ID to this widget's settings
|
|
||||||
updateWidgetPciId(firstGpu.pciId)
|
|
||||||
DgopService.addGpuPciId(firstGpu.pciId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateWidgetPciId(pciId) {
|
|
||||||
// Find and update this widget's pciId in the settings
|
|
||||||
var sections = ["left", "center", "right"]
|
|
||||||
for (var s = 0; s < sections.length; s++) {
|
|
||||||
var sectionId = sections[s]
|
|
||||||
var widgets = []
|
|
||||||
if (sectionId === "left")
|
|
||||||
widgets = SettingsData.topBarLeftWidgets.slice()
|
|
||||||
else if (sectionId === "center")
|
|
||||||
widgets = SettingsData.topBarCenterWidgets.slice()
|
|
||||||
else if (sectionId === "right")
|
|
||||||
widgets = SettingsData.topBarRightWidgets.slice()
|
|
||||||
|
|
||||||
for (var i = 0; i < widgets.length; i++) {
|
|
||||||
var widget = widgets[i]
|
|
||||||
if (typeof widget === "object" && widget.id === "gpuTemp"
|
|
||||||
&& (!widget.pciId || widget.pciId === "")) {
|
|
||||||
widgets[i] = {
|
|
||||||
"id": widget.id,
|
|
||||||
"enabled": widget.enabled !== undefined ? widget.enabled : true,
|
|
||||||
"selectedGpuIndex": 0,
|
|
||||||
"pciId": pciId
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sectionId === "left")
|
|
||||||
SettingsData.setTopBarLeftWidgets(widgets)
|
|
||||||
else if (sectionId === "center")
|
|
||||||
SettingsData.setTopBarCenterWidgets(widgets)
|
|
||||||
else if (sectionId === "right")
|
|
||||||
SettingsData.setTopBarRightWidgets(widgets)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: idleIcon.width + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : (SessionService.idleInhibited ? Theme.primaryHover : Theme.secondaryHover)
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: idleIcon
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
SessionService.toggleIdleInhibit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: contentRow.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
NiriService.cycleKeyboardLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: NiriService.getCurrentKeyboardLayoutName()
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "left"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked
|
|
||||||
|
|
||||||
width: Theme.iconSize + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: launcherArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
root.clicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: launcherContent
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: SettingsData.topBarNoBackground ? "transparent" : Qt.rgba(Theme.surfaceTextHover.r, Theme.surfaceTextHover.g,
|
|
||||||
Theme.surfaceTextHover.b,
|
|
||||||
Theme.surfaceTextHover.a * Theme.widgetTransparency)
|
|
||||||
|
|
||||||
SystemLogo {
|
|
||||||
visible: SettingsData.useOSLogo
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: Theme.iconSize - 3
|
|
||||||
height: Theme.iconSize - 3
|
|
||||||
colorOverride: SettingsData.osLogoColorOverride
|
|
||||||
brightnessOverride: SettingsData.osLogoBrightness
|
|
||||||
contrastOverride: SettingsData.osLogoContrast
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
visible: !SettingsData.useOSLogo
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "apps"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
|
||||||
readonly property bool playerAvailable: activePlayer !== null
|
|
||||||
property bool compactMode: false
|
|
||||||
readonly property int textWidth: {
|
|
||||||
switch (SettingsData.mediaSize) {
|
|
||||||
case 0:
|
|
||||||
return 0 // No text in small mode
|
|
||||||
case 2:
|
|
||||||
return 180 // Large text area
|
|
||||||
default:
|
|
||||||
return 120 // Medium text area
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property int currentContentWidth: {
|
|
||||||
// Calculate actual content width:
|
|
||||||
// AudioViz (20) + spacing + [text + spacing] + controls (prev:20 + spacing + play:24 + spacing + next:20) + padding
|
|
||||||
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20 // ~72px total
|
|
||||||
const audioVizWidth = 20
|
|
||||||
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth
|
|
||||||
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0) + horizontalPadding * 2
|
|
||||||
}
|
|
||||||
property string section: "center"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked
|
|
||||||
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = Theme.surfaceTextHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
name: "shown"
|
|
||||||
when: playerAvailable
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
opacity: 1
|
|
||||||
width: currentContentWidth
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "hidden"
|
|
||||||
when: !playerAvailable
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
opacity: 0
|
|
||||||
width: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
transitions: [
|
|
||||||
Transition {
|
|
||||||
from: "shown"
|
|
||||||
to: "hidden"
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
PauseAnimation {
|
|
||||||
duration: 500
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "opacity,width"
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Transition {
|
|
||||||
from: "hidden"
|
|
||||||
to: "shown"
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "opacity,width"
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: mediaRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: mediaInfo
|
|
||||||
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
AudioVisualization {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: textContainer
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: textWidth
|
|
||||||
height: 20
|
|
||||||
visible: SettingsData.mediaSize > 0
|
|
||||||
clip: true
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
property string displayText: {
|
|
||||||
if (!activePlayer || !activePlayer.trackTitle)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
let identity = activePlayer.identity || ""
|
|
||||||
let isWebMedia = identity.toLowerCase().includes("firefox")
|
|
||||||
|| identity.toLowerCase().includes(
|
|
||||||
"chrome") || identity.toLowerCase(
|
|
||||||
).includes("chromium")
|
|
||||||
|| identity.toLowerCase().includes(
|
|
||||||
"edge") || identity.toLowerCase().includes("safari")
|
|
||||||
let title = ""
|
|
||||||
let subtitle = ""
|
|
||||||
if (isWebMedia && activePlayer.trackTitle) {
|
|
||||||
title = activePlayer.trackTitle
|
|
||||||
subtitle = activePlayer.trackArtist || identity
|
|
||||||
} else {
|
|
||||||
title = activePlayer.trackTitle || "Unknown Track"
|
|
||||||
subtitle = activePlayer.trackArtist || ""
|
|
||||||
}
|
|
||||||
return subtitle.length > 0 ? title + " • " + subtitle : title
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: mediaText
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: textContainer.displayText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
|
|
||||||
property bool needsScrolling: implicitWidth > textContainer.width
|
|
||||||
property real scrollOffset: 0
|
|
||||||
|
|
||||||
x: needsScrolling ? -scrollOffset : 0
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: scrollAnimation
|
|
||||||
running: mediaText.needsScrolling
|
|
||||||
&& textContainer.visible
|
|
||||||
loops: Animation.Infinite
|
|
||||||
|
|
||||||
PauseAnimation {
|
|
||||||
duration: 2000
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: mediaText
|
|
||||||
property: "scrollOffset"
|
|
||||||
from: 0
|
|
||||||
to: mediaText.implicitWidth - textContainer.width + 5
|
|
||||||
duration: Math.max(
|
|
||||||
1000,
|
|
||||||
(mediaText.implicitWidth - textContainer.width + 5) * 60)
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
}
|
|
||||||
|
|
||||||
PauseAnimation {
|
|
||||||
duration: 2000
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: mediaText
|
|
||||||
property: "scrollOffset"
|
|
||||||
to: 0
|
|
||||||
duration: Math.max(
|
|
||||||
1000,
|
|
||||||
(mediaText.implicitWidth - textContainer.width + 5) * 60)
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextChanged: {
|
|
||||||
scrollOffset = 0
|
|
||||||
scrollAnimation.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.playerAvailable && root.opacity > 0
|
|
||||||
&& root.width > 0 && textContainer.visible
|
|
||||||
hoverEnabled: enabled
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onPressed: {
|
|
||||||
if (root.popupTarget
|
|
||||||
&& root.popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = root.parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
root.popupTarget.setTriggerPosition(
|
|
||||||
relativeX,
|
|
||||||
barHeight + Theme.spacingXS,
|
|
||||||
root.width, root.section, currentScreen)
|
|
||||||
}
|
|
||||||
root.clicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
radius: 10
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: prevArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
visible: root.playerAvailable
|
|
||||||
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_previous"
|
|
||||||
size: 12
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: prevArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.playerAvailable && root.width > 0
|
|
||||||
hoverEnabled: enabled
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: {
|
|
||||||
if (activePlayer)
|
|
||||||
activePlayer.previous()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
radius: 12
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: activePlayer
|
|
||||||
&& activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
|
|
||||||
visible: root.playerAvailable
|
|
||||||
opacity: activePlayer ? 1 : 0.3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: activePlayer
|
|
||||||
&& activePlayer.playbackState === 1 ? "pause" : "play_arrow"
|
|
||||||
size: 14
|
|
||||||
color: activePlayer
|
|
||||||
&& activePlayer.playbackState === 1 ? Theme.background : Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.playerAvailable && root.width > 0
|
|
||||||
hoverEnabled: enabled
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: {
|
|
||||||
if (activePlayer)
|
|
||||||
activePlayer.togglePlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
radius: 10
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: nextArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
visible: playerAvailable
|
|
||||||
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_next"
|
|
||||||
size: 12
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nextArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.playerAvailable && root.width > 0
|
|
||||||
hoverEnabled: enabled
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: {
|
|
||||||
if (activePlayer)
|
|
||||||
activePlayer.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int availableWidth: 400
|
|
||||||
readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2
|
|
||||||
readonly property int maxNormalWidth: 456
|
|
||||||
|
|
||||||
function formatNetworkSpeed(bytesPerSec) {
|
|
||||||
if (bytesPerSec < 1024)
|
|
||||||
return bytesPerSec.toFixed(0) + " B/s"
|
|
||||||
else if (bytesPerSec < 1024 * 1024)
|
|
||||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
|
|
||||||
else if (bytesPerSec < 1024 * 1024 * 1024)
|
|
||||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
|
|
||||||
else
|
|
||||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: contentRow.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = networkArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["network"])
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["network"])
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: networkArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "network_check"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "↓"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.info
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(
|
|
||||||
DgopService.networkRxRate) : "0 B/s"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "↑"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.error
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(
|
|
||||||
DgopService.networkTxRate) : "0 B/s"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked
|
|
||||||
|
|
||||||
width: notepadIcon.width + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = notepadArea.containsMouse
|
|
||||||
|| root.isActive ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: notepadIcon
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "assignment"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: notepadArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 6
|
|
||||||
height: 6
|
|
||||||
radius: 3
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.rightMargin: SettingsData.topBarNoBackground ? 0 : 4
|
|
||||||
anchors.topMargin: SettingsData.topBarNoBackground ? 0 : 4
|
|
||||||
visible: SessionData.notepadContent.length > 0
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: notepadArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.clicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool hasUnread: false
|
|
||||||
property bool isActive: false
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barHeight: 48
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
signal clicked
|
|
||||||
|
|
||||||
width: notificationIcon.width + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = notificationArea.containsMouse
|
|
||||||
|| root.isActive ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: notificationIcon
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: SessionData.doNotDisturb ? Theme.error : (notificationArea.containsMouse
|
|
||||||
|| root.isActive ? Theme.primary : Theme.surfaceText)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 8
|
|
||||||
height: 8
|
|
||||||
radius: 4
|
|
||||||
color: Theme.error
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.rightMargin: SettingsData.topBarNoBackground ? 0 : 6
|
|
||||||
anchors.topMargin: SettingsData.topBarNoBackground ? 0 : 6
|
|
||||||
visible: root.hasUnread
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: notificationArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
root.clicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
|
|
||||||
readonly property bool hasActivePrivacy: PrivacyService.anyPrivacyActive
|
|
||||||
readonly property int activeCount: PrivacyService.microphoneActive + PrivacyService.cameraActive
|
|
||||||
+ PrivacyService.screensharingActive
|
|
||||||
readonly property real contentWidth: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
|
|
||||||
|
|
||||||
width: hasActivePrivacy ? (contentWidth + horizontalPadding * 2) : 0
|
|
||||||
height: hasActivePrivacy ? widgetHeight : 0
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
visible: hasActivePrivacy
|
|
||||||
opacity: hasActivePrivacy ? 1 : 0
|
|
||||||
enabled: hasActivePrivacy
|
|
||||||
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
return Qt.rgba(
|
|
||||||
privacyArea.containsMouse ? Theme.errorPressed.r : Theme.errorHover.r,
|
|
||||||
privacyArea.containsMouse ? Theme.errorPressed.g : Theme.errorHover.g,
|
|
||||||
privacyArea.containsMouse ? Theme.errorPressed.b : Theme.errorHover.b,
|
|
||||||
(privacyArea.containsMouse ? Theme.errorPressed.a : Theme.errorHover.a)
|
|
||||||
* Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: privacyArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: hasActivePrivacy
|
|
||||||
enabled: hasActivePrivacy
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: hasActivePrivacy
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
visible: PrivacyService.microphoneActive
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "mic"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.error
|
|
||||||
filled: true
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
visible: PrivacyService.cameraActive
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "camera_video"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
filled: true
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 6
|
|
||||||
height: 6
|
|
||||||
radius: 3
|
|
||||||
color: Theme.error
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.topMargin: -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
visible: PrivacyService.screensharingActive
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "screen_share"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.warning
|
|
||||||
filled: true
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
enabled: hasActivePrivacy && visible
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: tooltip
|
|
||||||
|
|
||||||
width: tooltipText.contentWidth + Theme.spacingM * 2
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
visible: false
|
|
||||||
opacity: privacyArea.containsMouse && hasActivePrivacy ? 1 : 0
|
|
||||||
z: 100
|
|
||||||
|
|
||||||
x: (parent.width - width) / 2
|
|
||||||
y: -height - Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: PrivacyService.getPrivacySummary()
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
enabled: hasActivePrivacy && root.visible
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 8
|
|
||||||
height: 8
|
|
||||||
color: parent.color
|
|
||||||
border.color: parent.border.color
|
|
||||||
border.width: parent.border.width
|
|
||||||
rotation: 45
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.top: parent.bottom
|
|
||||||
anchors.topMargin: -4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool showPercentage: true
|
|
||||||
property bool showIcon: true
|
|
||||||
property var toggleProcessList
|
|
||||||
property string section: "right"
|
|
||||||
property var popupTarget: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real barHeight: 48
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
|
|
||||||
width: ramContent.implicitWidth + horizontalPadding * 2
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = ramArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
|
||||||
baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
DgopService.addRef(["memory"])
|
|
||||||
}
|
|
||||||
Component.onDestruction: {
|
|
||||||
DgopService.removeRef(["memory"])
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: ramArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
|
||||||
var globalPos = mapToGlobal(0, 0)
|
|
||||||
var currentScreen = parentScreen || Screen
|
|
||||||
var screenX = currentScreen.x || 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
popupTarget.setTriggerPosition(
|
|
||||||
relativeX, barHeight + Theme.spacingXS,
|
|
||||||
width, section, currentScreen)
|
|
||||||
}
|
|
||||||
DgopService.setSortBy("memory")
|
|
||||||
if (root.toggleProcessList)
|
|
||||||
root.toggleProcessList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: ramContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 3
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "developer_board"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: {
|
|
||||||
if (DgopService.memoryUsage > 90)
|
|
||||||
return Theme.tempDanger
|
|
||||||
|
|
||||||
if (DgopService.memoryUsage > 75)
|
|
||||||
return Theme.tempWarning
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (DgopService.memoryUsage === undefined
|
|
||||||
|| DgopService.memoryUsage === null
|
|
||||||
|| DgopService.memoryUsage === 0) {
|
|
||||||
return "--%"
|
|
||||||
}
|
|
||||||
return DgopService.memoryUsage.toFixed(0) + "%"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string section: "left"
|
|
||||||
property var parentScreen
|
|
||||||
property var hoveredItem: null
|
|
||||||
property var topBar: null
|
|
||||||
property real widgetHeight: 30
|
|
||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
// The visual root for this window
|
|
||||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
|
||||||
readonly property var sortedToplevels: {
|
|
||||||
if (SettingsData.runningAppsCurrentWorkspace){
|
|
||||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen.name)
|
|
||||||
}
|
|
||||||
return CompositorService.sortedToplevels
|
|
||||||
}
|
|
||||||
readonly property int windowCount: sortedToplevels.length
|
|
||||||
readonly property int calculatedWidth: {
|
|
||||||
if (windowCount === 0)
|
|
||||||
return 0
|
|
||||||
if (SettingsData.runningAppsCompactMode) {
|
|
||||||
return windowCount * 24 + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
|
|
||||||
} else {
|
|
||||||
return windowCount * (24 + Theme.spacingXS + 120)
|
|
||||||
+ (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: calculatedWidth
|
|
||||||
height: widgetHeight
|
|
||||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
visible: windowCount > 0
|
|
||||||
clip: false
|
|
||||||
color: {
|
|
||||||
if (windowCount === 0)
|
|
||||||
return "transparent"
|
|
||||||
|
|
||||||
if (SettingsData.topBarNoBackground) return "transparent"
|
|
||||||
const baseColor = Theme.secondaryHover
|
|
||||||
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
|
|
||||||
|
|
||||||
var windows = root.sortedToplevels;
|
|
||||||
if (windows.length < 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMouseWheel) {
|
|
||||||
// Direct mouse wheel action
|
|
||||||
var currentIndex = -1;
|
|
||||||
for (var i = 0; i < windows.length; i++) {
|
|
||||||
if (windows[i].activated) {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextWindow = windows[nextIndex];
|
|
||||||
if (nextWindow) {
|
|
||||||
nextWindow.activate();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Touchpad - accumulate small deltas
|
|
||||||
scrollAccumulator += deltaY
|
|
||||||
|
|
||||||
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
|
|
||||||
var currentIndex = -1;
|
|
||||||
for (var i = 0; i < windows.length; i++) {
|
|
||||||
if (windows[i].activated) {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextWindow = windows[nextIndex];
|
|
||||||
if (nextWindow) {
|
|
||||||
nextWindow.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollAccumulator = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wheel.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: windowRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: windowRepeater
|
|
||||||
|
|
||||||
model: sortedToplevels
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateItem
|
|
||||||
|
|
||||||
property bool isFocused: modelData.activated
|
|
||||||
property string appId: modelData.appId || ""
|
|
||||||
property string windowTitle: modelData.title || "(Unnamed)"
|
|
||||||
property var toplevelObject: modelData
|
|
||||||
property string tooltipText: {
|
|
||||||
var appName = "Unknown"
|
|
||||||
if (appId) {
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(appId)
|
|
||||||
appName = desktopEntry
|
|
||||||
&& desktopEntry.name ? desktopEntry.name : appId
|
|
||||||
}
|
|
||||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
|
||||||
}
|
|
||||||
|
|
||||||
width: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (isFocused)
|
|
||||||
return mouseArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.3) : Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.2)
|
|
||||||
else
|
|
||||||
return mouseArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primaryHover.r,
|
|
||||||
Theme.primaryHover.g,
|
|
||||||
Theme.primaryHover.b,
|
|
||||||
0.1) : "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: Quickshell.iconPath(DesktopEntries.heuristicLookup(Paths.moddedAppId(appId))?.icon, true)
|
|
||||||
smooth: true
|
|
||||||
mipmap: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback text if no icon found
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: !iconImg.visible
|
|
||||||
text: {
|
|
||||||
if (!appId)
|
|
||||||
return "?"
|
|
||||||
|
|
||||||
var desktopEntry = DesktopEntries.heuristicLookup(appId)
|
|
||||||
if (desktopEntry && desktopEntry.name)
|
|
||||||
return desktopEntry.name.charAt(0).toUpperCase()
|
|
||||||
|
|
||||||
return appId.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
// Window title text (only visible in expanded mode)
|
|
||||||
StyledText {
|
|
||||||
anchors.left: iconImg.right
|
|
||||||
anchors.leftMargin: Theme.spacingXS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !SettingsData.runningAppsCompactMode
|
|
||||||
text: windowTitle
|
|
||||||
font.pixelSize: Theme.fontSizeMedium - 1
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
if (toplevelObject) {
|
|
||||||
toplevelObject.activate()
|
|
||||||
}
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
if (tooltipLoader.item)
|
|
||||||
tooltipLoader.item.hideTooltip()
|
|
||||||
tooltipLoader.active = false
|
|
||||||
|
|
||||||
windowContextMenuLoader.active = true
|
|
||||||
if (windowContextMenuLoader.item) {
|
|
||||||
windowContextMenuLoader.item.currentWindow = toplevelObject
|
|
||||||
var globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
|
|
||||||
var screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
var screenY = root.parentScreen ? root.parentScreen.y : 0
|
|
||||||
var relativeX = globalPos.x - screenX
|
|
||||||
var yPos = Theme.barHeight + SettingsData.topBarSpacing - 7
|
|
||||||
windowContextMenuLoader.item.showAt(relativeX, yPos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onEntered: {
|
|
||||||
root.hoveredItem = delegateItem
|
|
||||||
var globalPos = delegateItem.mapToGlobal(
|
|
||||||
delegateItem.width / 2, delegateItem.height)
|
|
||||||
tooltipLoader.active = true
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
var tooltipY = Theme.barHeight
|
|
||||||
+ SettingsData.topBarSpacing + Theme.spacingXS
|
|
||||||
tooltipLoader.item.showTooltip(
|
|
||||||
delegateItem.tooltipText, globalPos.x,
|
|
||||||
tooltipY, root.parentScreen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
if (root.hoveredItem === delegateItem) {
|
|
||||||
root.hoveredItem = null
|
|
||||||
if (tooltipLoader.item)
|
|
||||||
tooltipLoader.item.hideTooltip()
|
|
||||||
|
|
||||||
tooltipLoader.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: tooltipLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
|
|
||||||
sourceComponent: RunningAppsTooltip {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: windowContextMenuLoader
|
|
||||||
active: false
|
|
||||||
sourceComponent: PanelWindow {
|
|
||||||
id: contextMenuWindow
|
|
||||||
|
|
||||||
property var currentWindow: null
|
|
||||||
property bool isVisible: false
|
|
||||||
property point anchorPos: Qt.point(0, 0)
|
|
||||||
|
|
||||||
function showAt(x, y) {
|
|
||||||
screen = root.parentScreen
|
|
||||||
anchorPos = Qt.point(x, y)
|
|
||||||
isVisible = true
|
|
||||||
visible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
isVisible = false
|
|
||||||
visible = false
|
|
||||||
windowContextMenuLoader.active = false
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitWidth: 100
|
|
||||||
implicitHeight: 40
|
|
||||||
visible: false
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: contextMenuWindow.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
x: {
|
|
||||||
var left = 10
|
|
||||||
var right = contextMenuWindow.width - width - 10
|
|
||||||
var want = contextMenuWindow.anchorPos.x - width / 2
|
|
||||||
return Math.max(left, Math.min(right, want))
|
|
||||||
}
|
|
||||||
y: contextMenuWindow.anchorPos.y
|
|
||||||
width: 100
|
|
||||||
height: 32
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: parent.radius
|
|
||||||
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Close"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: closeMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (contextMenuWindow.currentWindow) {
|
|
||||||
contextMenuWindow.currentWindow.close()
|
|
||||||
}
|
|
||||||
contextMenuWindow.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string tooltipText: ""
|
|
||||||
property real targetX: 0
|
|
||||||
property real targetY: 0
|
|
||||||
property var targetScreen: null
|
|
||||||
|
|
||||||
function showTooltip(text, x, y, screen) {
|
|
||||||
tooltipText = text
|
|
||||||
targetScreen = screen
|
|
||||||
|
|
||||||
var screenX = screen ? screen.x : 0
|
|
||||||
targetX = x - screenX
|
|
||||||
targetY = y
|
|
||||||
|
|
||||||
visible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideTooltip() {
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: targetScreen
|
|
||||||
implicitWidth: Math.min(300, Math.max(
|
|
||||||
120,
|
|
||||||
textContent.implicitWidth + Theme.spacingM * 2))
|
|
||||||
implicitHeight: textContent.implicitHeight + Theme.spacingS * 2
|
|
||||||
color: "transparent"
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
}
|
|
||||||
|
|
||||||
margins {
|
|
||||||
left: Math.round(targetX - implicitWidth / 2)
|
|
||||||
top: Math.round(targetY)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: textContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.tooltipText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user