Compare commits
1457 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b22069c455 | ||
|
|
b391d03967 | ||
|
|
adb84a9e24 | ||
|
|
4b84e48e8e | ||
|
|
20cbc03b22 | ||
|
|
aa33747686 | ||
|
|
49a0c8449f | ||
|
|
88871e3fbe | ||
|
|
b3989a13da | ||
|
|
07a94de5e2 | ||
|
|
994f0ca812 | ||
|
|
1c1cb8e026 | ||
|
|
74270e9478 | ||
|
|
8c9396f325 | ||
|
|
afccf048e7 | ||
|
|
f37625719d | ||
|
|
cad8fd671f | ||
|
|
68e76abfc7 | ||
|
|
45c8fe7782 | ||
|
|
5ebf4b5377 | ||
|
|
59fbe92fe4 | ||
|
|
b051e19f68 | ||
|
|
6b9370ac85 | ||
|
|
9702a300ca | ||
|
|
b043664617 | ||
|
|
368e80daf2 | ||
|
|
056217bf43 | ||
|
|
c1abb3a7dc | ||
|
|
52d2055699 | ||
|
|
e324a33137 | ||
|
|
6f4aa1a1a1 | ||
|
|
f49462f999 | ||
|
|
4fb1e2de1e | ||
|
|
6d05a20556 | ||
|
|
ec2fbb53dc | ||
|
|
fdc61acfe4 | ||
|
|
32712c7052 | ||
|
|
a0f6d14334 | ||
|
|
6ae8d8536e | ||
|
|
650dcb8811 | ||
|
|
970684e304 | ||
|
|
e786946abf | ||
|
|
da046cade6 | ||
|
|
43dee793de | ||
|
|
0a893f9c5f | ||
|
|
23887574cf | ||
|
|
2008ba85bc | ||
|
|
773318191d | ||
|
|
78cf0bc8a2 | ||
|
|
8b0e0f6e0e | ||
|
|
8c6b3a793f | ||
|
|
4c3eca80a4 | ||
|
|
f61f9a5809 | ||
|
|
518e90d910 | ||
|
|
d2e5d0664a | ||
|
|
602d79c98e | ||
|
|
4b13e89a64 | ||
|
|
1e8b122911 | ||
|
|
1f257ce847 | ||
|
|
df35589328 | ||
|
|
c92478d27d | ||
|
|
ffe39e0ec9 | ||
|
|
b12cf345dc | ||
|
|
fc4418be0c | ||
|
|
82bfa346a7 | ||
|
|
26ee5046f6 | ||
|
|
51ed6ea2b0 | ||
|
|
c53dd6fade | ||
|
|
bb24b6904d | ||
|
|
d5857e3363 | ||
|
|
559609be64 | ||
|
|
5cea61114b | ||
|
|
22794ea922 | ||
|
|
933ba54612 | ||
|
|
0d0b9a21f2 | ||
|
|
9ed9231070 | ||
|
|
b8b54825d5 | ||
|
|
250822e819 | ||
|
|
ece9789f6b | ||
|
|
f11d27bcf1 | ||
|
|
0e69256279 | ||
|
|
fa49d4aaa0 | ||
|
|
b1f7ae5d9a | ||
|
|
e6b0be77e7 | ||
|
|
49961882dd | ||
|
|
c1d2d82fa2 | ||
|
|
c35f37c7d7 | ||
|
|
e23cb90c5b | ||
|
|
b2688e9100 | ||
|
|
7f3842ddbf | ||
|
|
68b2c83be1 | ||
|
|
97fa2fb1b5 | ||
|
|
0ed8ed7fe5 | ||
|
|
a41be0b5d9 | ||
|
|
072d80e2f3 | ||
|
|
1f898171e0 | ||
|
|
ef64395dd4 | ||
|
|
a5c89fadb5 | ||
|
|
cccf0e6017 | ||
|
|
5da474007e | ||
|
|
ffd2cdaf74 | ||
|
|
5f3c088f22 | ||
|
|
382116e795 | ||
|
|
c7c49433f7 | ||
|
|
0d2d0f1931 | ||
|
|
2e947edc5a | ||
|
|
cdc32f3eac | ||
|
|
21736b3095 | ||
|
|
48852a9ca4 | ||
|
|
65fab7b367 | ||
|
|
dc414df9bc | ||
|
|
69a6c052db | ||
|
|
c422435d3d | ||
|
|
fc1742e167 | ||
|
|
061e7f32da | ||
|
|
8dda007847 | ||
|
|
1cdff28cca | ||
|
|
f32a34e320 | ||
|
|
0d0088bd52 | ||
|
|
a7a7a96585 | ||
|
|
026d602770 | ||
|
|
5b54be633d | ||
|
|
3bb10e9561 | ||
|
|
b9b233a873 | ||
|
|
388824bf37 | ||
|
|
25eb31747a | ||
|
|
f7109b0bf9 | ||
|
|
c41fa1aef7 | ||
|
|
1a0ea3893c | ||
|
|
0593543d7a | ||
|
|
fbf80ab577 | ||
|
|
7e9f7f40ef | ||
|
|
92460fc5c3 | ||
|
|
c1c91edb6c | ||
|
|
e73d85de04 | ||
|
|
fafd7a518b | ||
|
|
8b89e95b13 | ||
|
|
2112f675c0 | ||
|
|
d873c2205b | ||
|
|
348c1e8f9f | ||
|
|
8e248f6795 | ||
|
|
4c516200dc | ||
|
|
b5b8b62cf0 | ||
|
|
a4b4caa2ce | ||
|
|
423ea60939 | ||
|
|
55dd48ce66 | ||
|
|
7dc8d2cd88 | ||
|
|
d4dd3b1734 | ||
|
|
0f30a10a14 | ||
|
|
50e2a95f52 | ||
|
|
35bf30ef5e | ||
|
|
afce091473 | ||
|
|
d802b6a2fa | ||
|
|
65cd95c62b | ||
|
|
fe2654268d | ||
|
|
13e32dc11b | ||
|
|
b27728e5bf | ||
|
|
2379ad134b | ||
|
|
3ab9ffed78 | ||
|
|
3182d1969b | ||
|
|
591d099255 | ||
|
|
256f9b4a76 | ||
|
|
dd29a739f3 | ||
|
|
83d82a825b | ||
|
|
e2f7012c5b | ||
|
|
ff1509939a | ||
|
|
f8ee0bb8df | ||
|
|
96d3051151 | ||
|
|
e8e96a9f68 | ||
|
|
b7c99905f3 | ||
|
|
ab89b0e964 | ||
|
|
7b9ecd048d | ||
|
|
9d30eac13a | ||
|
|
4785e287ba | ||
|
|
aa1cea8d03 | ||
|
|
823ab9c6a3 | ||
|
|
74a0c9dbf4 | ||
|
|
d1a89387f9 | ||
|
|
9da310ade4 | ||
|
|
348604e45a | ||
|
|
5e44af8e6d | ||
|
|
27eaeee5fd | ||
|
|
338f4cde6d | ||
|
|
1531275707 | ||
|
|
5cfa66f9e8 | ||
|
|
695d002d6a | ||
|
|
7afd0177cb | ||
|
|
180366073f | ||
|
|
7eb19237ba | ||
|
|
ed7b4f5552 | ||
|
|
9d927bd7fc | ||
|
|
ac683caa1e | ||
|
|
39883ceb10 | ||
|
|
c1386c491e | ||
|
|
e7f8a452b8 | ||
|
|
012ae28dd9 | ||
|
|
95d059007e | ||
|
|
b76a252b94 | ||
|
|
6bd4167638 | ||
|
|
22b843587c | ||
|
|
cb3fc1a45c | ||
|
|
b1df7624cc | ||
|
|
8be64359ef | ||
|
|
8e6badc0d6 | ||
|
|
4ac27be0e8 | ||
|
|
2a496a7831 | ||
|
|
619420349c | ||
|
|
349ef85648 | ||
|
|
b38cf8ef66 | ||
|
|
23c83a49c3 | ||
|
|
1926008315 | ||
|
|
deb75f5bab | ||
|
|
53baf1c86b | ||
|
|
8173919692 | ||
|
|
ece8705e5d | ||
|
|
346d29d94a | ||
|
|
a3f604efc3 | ||
|
|
0e8a920ee2 | ||
|
|
e98e034a68 | ||
|
|
1f3cafb1b9 | ||
|
|
316cd3114a | ||
|
|
4c951cf380 | ||
|
|
0f888fd734 | ||
|
|
0690ac4996 | ||
|
|
3809f290ed | ||
|
|
b1094bbfa0 | ||
|
|
644e24f409 | ||
|
|
6f2d7516f0 | ||
|
|
8dad25f79c | ||
|
|
4a9f37a390 | ||
|
|
36489491e4 | ||
|
|
2c7038c504 | ||
|
|
846730361d | ||
|
|
428f3627b6 | ||
|
|
68b328c982 | ||
|
|
4dac2ffe88 | ||
|
|
f3535f22ba | ||
|
|
deca5e1235 | ||
|
|
8da903bb61 | ||
|
|
b58f6f0a1b | ||
|
|
946996917d | ||
|
|
b03b4b0f13 | ||
|
|
73f76e2275 | ||
|
|
80442e2839 | ||
|
|
a8a1b0a422 | ||
|
|
346e27830a | ||
|
|
ef616efcca | ||
|
|
8c1153192d | ||
|
|
c46a84d794 | ||
|
|
46d3465b50 | ||
|
|
7bd278d428 | ||
|
|
2123b55aab | ||
|
|
4de6489cbf | ||
|
|
96c2817e06 | ||
|
|
35a7ed165f | ||
|
|
1c5b02fab4 | ||
|
|
2afec4cc46 | ||
|
|
6dd6c6af74 | ||
|
|
d86686704c | ||
|
|
22b8edb023 | ||
|
|
b96deaa0c3 | ||
|
|
0cb619a787 | ||
|
|
63951ced9e | ||
|
|
84502f4c9f | ||
|
|
430cc64fdb | ||
|
|
b93c733e7c | ||
|
|
fe58e5e92a | ||
|
|
e6ae17cdd5 | ||
|
|
b445153444 | ||
|
|
6f85747d92 | ||
|
|
66360c2379 | ||
|
|
7fe504aa8a | ||
|
|
aca831e54d | ||
|
|
7da4b1d63c | ||
|
|
f21bda0de9 | ||
|
|
24ffedd599 | ||
|
|
7f9acccce7 | ||
|
|
084fb39abd | ||
|
|
06694f2428 | ||
|
|
9105ec6b0d | ||
|
|
9cfe49dec3 | ||
|
|
58fb397e79 | ||
|
|
5de4330199 | ||
|
|
5669debd6b | ||
|
|
e71335f9b6 | ||
|
|
24cb5823ee | ||
|
|
1470a92556 | ||
|
|
1d98a657b2 | ||
|
|
2e1f6f0323 | ||
|
|
04f247905a | ||
|
|
2bfed74851 | ||
|
|
2a23b6afdd | ||
|
|
df70f0c824 | ||
|
|
2285a3fb18 | ||
|
|
ef5447d2fa | ||
|
|
fb64b3ba43 | ||
|
|
1673201916 | ||
|
|
72475cd29b | ||
|
|
41b9eb1897 | ||
|
|
31db195087 | ||
|
|
9a9d68c78d | ||
|
|
a2b57c5165 | ||
|
|
e9efab0d59 | ||
|
|
5d58083ee5 | ||
|
|
055c7d3c20 | ||
|
|
0b5ef30b34 | ||
|
|
6d4ca4ffc0 | ||
|
|
4cd53c4083 | ||
|
|
c6303cdb6b | ||
|
|
c48e87e012 | ||
|
|
1ca84bf052 | ||
|
|
f86dac2172 | ||
|
|
59fe0a058e | ||
|
|
640a4339db | ||
|
|
505cf48b6c | ||
|
|
6d5574cac0 | ||
|
|
e35264708a | ||
|
|
ea0350bcca | ||
|
|
b47ac6dd8a | ||
|
|
120ed36deb | ||
|
|
26fe3114a6 | ||
|
|
39e58acade | ||
|
|
807e7394fe | ||
|
|
d745be9c96 | ||
|
|
8f8f6c23ea | ||
|
|
3da0e529c6 | ||
|
|
d5a862d904 | ||
|
|
4de2b7f5a8 | ||
|
|
9f31c61a18 | ||
|
|
d8539c0814 | ||
|
|
9b8c0b9cf0 | ||
|
|
c4764c0e5b | ||
|
|
aec170d7f8 | ||
|
|
a395156556 | ||
|
|
50ea3e9a8b | ||
|
|
50ef79677e | ||
|
|
def778dbf1 | ||
|
|
b8f4401878 | ||
|
|
9a7fb4a219 | ||
|
|
39b52eb17e | ||
|
|
609f1e9655 | ||
|
|
9bb60d0ae3 | ||
|
|
202516aee3 | ||
|
|
489ce76d2a | ||
|
|
6a8c3c721a | ||
|
|
21d331c232 | ||
|
|
4c9d40865f | ||
|
|
490200b3b8 | ||
|
|
6031c97e1a | ||
|
|
4d0777ab93 | ||
|
|
17308083fe | ||
|
|
51fb5b9f4a | ||
|
|
773912320f | ||
|
|
4a4cd20553 | ||
|
|
6fbaf46ed9 | ||
|
|
03da290c54 | ||
|
|
2d0d6207a1 | ||
|
|
f896b41c6b | ||
|
|
e0d577cbda | ||
|
|
be1c975f4d | ||
|
|
c20773d60b | ||
|
|
45fb881ec2 | ||
|
|
64001152ef | ||
|
|
5aa935b348 | ||
|
|
826dba7f53 | ||
|
|
358cfe26e2 | ||
|
|
8ece805273 | ||
|
|
8e32816976 | ||
|
|
64757979e8 | ||
|
|
26a4861a8b | ||
|
|
21c6c5a610 | ||
|
|
5594257147 | ||
|
|
879d9ec879 | ||
|
|
d13793fcbd | ||
|
|
51138cbf55 | ||
|
|
355473a946 | ||
|
|
f25bba7c11 | ||
|
|
f348eb993c | ||
|
|
3f1675b84a | ||
|
|
3aac552c44 | ||
|
|
1717fc0992 | ||
|
|
a7e3deecd3 | ||
|
|
46c3ea5d22 | ||
|
|
78f0c1da6a | ||
|
|
4753766b4f | ||
|
|
0c1ed01319 | ||
|
|
91dbc6a7f1 | ||
|
|
d4a46e5361 | ||
|
|
177a9743d6 | ||
|
|
2b8338938a | ||
|
|
84702465d7 | ||
|
|
3684c87f8c | ||
|
|
85815ba86d | ||
|
|
6eb453136d | ||
|
|
385f4943ae | ||
|
|
4dcc9609d6 | ||
|
|
3bbf26a18e | ||
|
|
dfe3aed46e | ||
|
|
796e080948 | ||
|
|
052bdefaab | ||
|
|
794853b7bd | ||
|
|
fbd431164b | ||
|
|
2c1c1a513a | ||
|
|
0279b5654a | ||
|
|
c93e907595 | ||
|
|
5965004721 | ||
|
|
86d891cfa8 | ||
|
|
1161fca422 | ||
|
|
26575ade7e | ||
|
|
fac9b8f54c | ||
|
|
71ce858b32 | ||
|
|
ff34696d28 | ||
|
|
2e0214ddb8 | ||
|
|
f316effecd | ||
|
|
6aa14120de | ||
|
|
1ad6969d9b | ||
|
|
aed7440c5b | ||
|
|
10534b46f9 | ||
|
|
802d4efdd3 | ||
|
|
20949a0298 | ||
|
|
8f596f14b0 | ||
|
|
c85043782f | ||
|
|
fe4603f87a | ||
|
|
f8313a04fd | ||
|
|
ba5e85ca67 | ||
|
|
5233547d76 | ||
|
|
56db321846 | ||
|
|
8d0ce8dc49 | ||
|
|
a340f8f31f | ||
|
|
3853c099d0 | ||
|
|
35a928e3d8 | ||
|
|
8d942d0782 | ||
|
|
c70a66b589 | ||
|
|
a8398916c9 | ||
|
|
ed464b196f | ||
|
|
f3f8b82fdd | ||
|
|
2cd73c265d | ||
|
|
737e990117 | ||
|
|
8a78ee090a | ||
|
|
761aa62995 | ||
|
|
dabf281ae8 | ||
|
|
5cb9935f2f | ||
|
|
9236b2f00e | ||
|
|
29b67f1337 | ||
|
|
dd2c02af3f | ||
|
|
b960441321 | ||
|
|
babb4ca202 | ||
|
|
4dc1076abc | ||
|
|
590708da57 | ||
|
|
78df416bc7 | ||
|
|
fcc054c3ae | ||
|
|
06b858a77e | ||
|
|
658b583e84 | ||
|
|
ed557af1c2 | ||
|
|
61203dc5fd | ||
|
|
b7d417ea91 | ||
|
|
978405bd85 | ||
|
|
878115db59 | ||
|
|
50469e5c82 | ||
|
|
860e721709 | ||
|
|
1dbc0cada6 | ||
|
|
88ece93db2 | ||
|
|
2d290bf5f7 | ||
|
|
891c8660e3 | ||
|
|
a734235cd0 | ||
|
|
8fdc6a0f72 | ||
|
|
603f499355 | ||
|
|
2b8b97ab3b | ||
|
|
458ef3c0d5 | ||
|
|
c4008e3899 | ||
|
|
6c3299ad10 | ||
|
|
6fe498ce19 | ||
|
|
4e67f26576 | ||
|
|
b2d46ab759 | ||
|
|
0d3cc917fa | ||
|
|
ac591da6c5 | ||
|
|
c7709b5f21 | ||
|
|
e6370904cd | ||
|
|
e412cee52f | ||
|
|
c3019230ae | ||
|
|
c7ab350cbd | ||
|
|
b65d82d895 | ||
|
|
89eb5ecde6 | ||
|
|
b374f167ef | ||
|
|
28026a4c37 | ||
|
|
b8bce3d421 | ||
|
|
6fba3457f7 | ||
|
|
07a6a16011 | ||
|
|
6b61599633 | ||
|
|
1bd093db7f | ||
|
|
3d9295856c | ||
|
|
a1aabd02f5 | ||
|
|
ae2d3eddd6 | ||
|
|
b75c358f54 | ||
|
|
0972a55aad | ||
|
|
112f71b633 | ||
|
|
e67d7166de | ||
|
|
6e88118ca9 | ||
|
|
75b7f0fcb0 | ||
|
|
47f72d9498 | ||
|
|
85d7dc2506 | ||
|
|
1305efec24 | ||
|
|
8af8bf2e2e | ||
|
|
abd6a66297 | ||
|
|
2e9a812513 | ||
|
|
8d845e7cd0 | ||
|
|
a1dcef8dec | ||
|
|
38e0bb8e64 | ||
|
|
8811cb3d13 | ||
|
|
a872682eb8 | ||
|
|
46b8317330 | ||
|
|
8204460112 | ||
|
|
292337dc00 | ||
|
|
0b790c219d | ||
|
|
7acca17b83 | ||
|
|
166da9191e | ||
|
|
de6b7c6470 | ||
|
|
a92b4b311a | ||
|
|
3a6bf8d299 | ||
|
|
cdca7c1d83 | ||
|
|
6f1ae43d62 | ||
|
|
eb26aa10f7 | ||
|
|
cdfb110007 | ||
|
|
b7d8f92414 | ||
|
|
b625df6484 | ||
|
|
2c3eb6efda | ||
|
|
8e034cd912 | ||
|
|
03bdfdb340 | ||
|
|
95d2dbe3fc | ||
|
|
071100459f | ||
|
|
0da59954cd | ||
|
|
2e63f93d41 | ||
|
|
c6ee99375d | ||
|
|
ed6562475d | ||
|
|
a2caebb8e5 | ||
|
|
03698e7bb9 | ||
|
|
d8db086127 | ||
|
|
339505abe3 | ||
|
|
b52451fde5 | ||
|
|
ac1902c76a | ||
|
|
93c674f356 | ||
|
|
5f3add5d99 | ||
|
|
937675ebb3 | ||
|
|
47ef62beb3 | ||
|
|
593a0bfc2c | ||
|
|
abe51f4928 | ||
|
|
33a75d042d | ||
|
|
2a62a13b16 | ||
|
|
4edeedd5ad | ||
|
|
8a17c047c9 | ||
|
|
899595ec5c | ||
|
|
dbf1020636 | ||
|
|
7def695c0e | ||
|
|
b51a87a981 | ||
|
|
3da2682111 | ||
|
|
758f2f2e55 | ||
|
|
26a27c3393 | ||
|
|
9d9bfb54e1 | ||
|
|
ab5b1e4d82 | ||
|
|
8cb9f04a22 | ||
|
|
22bc5a3bff | ||
|
|
f5982f41a2 | ||
|
|
9a80d51b10 | ||
|
|
fa838ecdb1 | ||
|
|
c0d6780c3d | ||
|
|
8935f9a0f9 | ||
|
|
b8b97c46a0 | ||
|
|
f2bbf70f93 | ||
|
|
ecf468f78f | ||
|
|
0b35fc1d2d | ||
|
|
94c5d73a61 | ||
|
|
f399a6d9f5 | ||
|
|
44fd859aec | ||
|
|
53d0c3943d | ||
|
|
d80f923802 | ||
|
|
1b861d7b7b | ||
|
|
65933208ec | ||
|
|
5df218a789 | ||
|
|
2f7a834b55 | ||
|
|
eca301553e | ||
|
|
91efa38101 | ||
|
|
acfe94f736 | ||
|
|
97bfcbb9e8 | ||
|
|
9e47d91be2 | ||
|
|
8872002225 | ||
|
|
519a85b251 | ||
|
|
5aa7ff7e91 | ||
|
|
5ce5659b38 | ||
|
|
00459606ce | ||
|
|
da1081700a | ||
|
|
7e965262f5 | ||
|
|
19312d94c3 | ||
|
|
118323e6b5 | ||
|
|
aed81e82b0 | ||
|
|
76c167c2c2 | ||
|
|
852e2fa4d1 | ||
|
|
17dceffff6 | ||
|
|
b589f37e0b | ||
|
|
682c6af231 | ||
|
|
d71226c6bd | ||
|
|
1f3725faf8 | ||
|
|
efcec1b2f9 | ||
|
|
651322aef0 | ||
|
|
a0a3a58668 | ||
|
|
b3abe44d65 | ||
|
|
5b603472bd | ||
|
|
57b0fe8a21 | ||
|
|
f5561da3cc | ||
|
|
11f6475b9f | ||
|
|
fb2c5e0470 | ||
|
|
b1764fddc8 | ||
|
|
bb7f957e44 | ||
|
|
0682315c9d | ||
|
|
dd100597ed | ||
|
|
6bc6380ee1 | ||
|
|
966089e471 | ||
|
|
3956461254 | ||
|
|
02d114a05e | ||
|
|
3764edafa8 | ||
|
|
1cd0376381 | ||
|
|
2b154e2cdb | ||
|
|
933dfc402b | ||
|
|
34d037d7dc | ||
|
|
76b6626073 | ||
|
|
d348cfc2b0 | ||
|
|
f9d7de2e3c | ||
|
|
2ea00fffa5 | ||
|
|
b163dab241 | ||
|
|
af0f4818d8 | ||
|
|
8b6c7632af | ||
|
|
d6d51d24c9 | ||
|
|
0c6aea7154 | ||
|
|
a61b2edd07 | ||
|
|
c108e7707a | ||
|
|
f3123ba5b1 | ||
|
|
c09a93af48 | ||
|
|
2a262999ce | ||
|
|
7d952dc226 | ||
|
|
3cb838b455 | ||
|
|
7594651e05 | ||
|
|
0d611fc891 | ||
|
|
8982909fae | ||
|
|
132b331c7c | ||
|
|
85cef214c8 | ||
|
|
80b4dad199 | ||
|
|
aadbc9596d | ||
|
|
0949d154c1 | ||
|
|
a86a0d33c1 | ||
|
|
e6372a2473 | ||
|
|
e3d9ab5679 | ||
|
|
ccd7458ea3 | ||
|
|
d41b59d563 | ||
|
|
290ba4ac03 | ||
|
|
1ee14df915 | ||
|
|
76376a9783 | ||
|
|
880ac93662 | ||
|
|
1157c8e21d | ||
|
|
2082cfe7c7 | ||
|
|
9a9f2886e0 | ||
|
|
0035fbcc4e | ||
|
|
5ca2c2a095 | ||
|
|
46103062d0 | ||
|
|
78a41c236c | ||
|
|
9dfac69e9e | ||
|
|
de72236fe5 | ||
|
|
101e3125a9 | ||
|
|
b443c9f492 | ||
|
|
2a1e7832d6 | ||
|
|
8c815146e6 | ||
|
|
acae2b8c21 | ||
|
|
004836fc8f | ||
|
|
b51f2d16cb | ||
|
|
6fba9d9f22 | ||
|
|
335e38d461 | ||
|
|
ee50d84a53 | ||
|
|
e706dabef3 | ||
|
|
dcedae46e5 | ||
|
|
f27f9d35b0 | ||
|
|
4f5acb7114 | ||
|
|
25ba27cbdd | ||
|
|
74da975ed4 | ||
|
|
6f6a5b364a | ||
|
|
f670f88804 | ||
|
|
814cb774a6 | ||
|
|
50d8b54adf | ||
|
|
ae931b791f | ||
|
|
dd4641eedd | ||
|
|
b66bb46fc1 | ||
|
|
5079fc78d3 | ||
|
|
7d2eaa46e6 | ||
|
|
1043eaa39f | ||
|
|
f16798f6e3 | ||
|
|
96acb1a679 | ||
|
|
0f93797ab3 | ||
|
|
3186a84d6b | ||
|
|
d3ee66d845 | ||
|
|
a8837283ab | ||
|
|
6fe0784c00 | ||
|
|
59ce164b40 | ||
|
|
70144eb06f | ||
|
|
5136af5d95 | ||
|
|
ff42244c6d | ||
|
|
3a2bb40117 | ||
|
|
bcd3100849 | ||
|
|
c8886629ad | ||
|
|
be4a69f6e0 | ||
|
|
62b12d5436 | ||
|
|
99e75d51b8 | ||
|
|
079c8f0803 | ||
|
|
16f87cbfa3 | ||
|
|
380f31fbd9 | ||
|
|
28677d6888 | ||
|
|
b9ae772987 | ||
|
|
4265290a0f | ||
|
|
07e94b0f0e | ||
|
|
307318918d | ||
|
|
3f6662182e | ||
|
|
be532fa146 | ||
|
|
722a59da80 | ||
|
|
3c97acf00f | ||
|
|
2d4fa59c41 | ||
|
|
c5ca758d3e | ||
|
|
6f70a98b83 | ||
|
|
424594a11a | ||
|
|
f5ac42c692 | ||
|
|
675f96d0e6 | ||
|
|
9570688294 | ||
|
|
626b745ce3 | ||
|
|
4afb98cf4c | ||
|
|
df2a9a246d | ||
|
|
d80e9ba3d5 | ||
|
|
130c68b3e2 | ||
|
|
6eea4a17a4 | ||
|
|
40dc8633ec | ||
|
|
12ac91d125 | ||
|
|
87d86911d7 | ||
|
|
2872a7b5c9 | ||
|
|
4067896434 | ||
|
|
78443451e4 | ||
|
|
719f5a20e7 | ||
|
|
d8b12e6d6b | ||
|
|
9a0746d737 | ||
|
|
77f8b3937c | ||
|
|
3f4313635a | ||
|
|
004d92a85d | ||
|
|
720c17258b | ||
|
|
a8b312f3a7 | ||
|
|
4d6361dfe5 | ||
|
|
1f75819795 | ||
|
|
50ddd2916c | ||
|
|
d30e14f611 | ||
|
|
227b0dd962 | ||
|
|
ac61086c95 | ||
|
|
0980f65751 | ||
|
|
7aa3da2ff4 | ||
|
|
83fbb8f95d | ||
|
|
a029463527 | ||
|
|
baafe54d13 | ||
|
|
a1cbd35202 | ||
|
|
1337a35a1e | ||
|
|
61006fbed0 | ||
|
|
eff4337d35 | ||
|
|
f0733f19dd | ||
|
|
818df48787 | ||
|
|
0eedfba071 | ||
|
|
2dc9e2f212 | ||
|
|
62a3b343cf | ||
|
|
76be93a84d | ||
|
|
b59c56170e | ||
|
|
c9285d8c5b | ||
|
|
b157d855a8 | ||
|
|
82ac49ce85 | ||
|
|
7247a26586 | ||
|
|
be0b568f1f | ||
|
|
e4b54e518c | ||
|
|
6ea1e2b4c7 | ||
|
|
5b4c57eae2 | ||
|
|
6e5efc3244 | ||
|
|
271a887bbf | ||
|
|
0571ba7325 | ||
|
|
c2f6c39016 | ||
|
|
2de2908509 | ||
|
|
99d56687ef | ||
|
|
434b8273f0 | ||
|
|
663382c81c | ||
|
|
3f388bdb4b | ||
|
|
0a4317f712 | ||
|
|
b9dbbf7bdd | ||
|
|
6ed9a8c5ae | ||
|
|
73de564bb6 | ||
|
|
1f62cdedb5 | ||
|
|
7ed0e894ec | ||
|
|
d39a9a85bf | ||
|
|
d16d1c1d26 | ||
|
|
291ffac102 | ||
|
|
2b18ed3c41 | ||
|
|
3b50efc7d0 | ||
|
|
d91a635781 | ||
|
|
4afe2d8448 | ||
|
|
74fce51c2d | ||
|
|
44cdbfe5d7 | ||
|
|
4fbb8314eb | ||
|
|
851a5a6f58 | ||
|
|
833808152e | ||
|
|
84706cab4b | ||
|
|
e571f26583 | ||
|
|
16cea533da | ||
|
|
b1f501f3f9 | ||
|
|
afcba942c7 | ||
|
|
5e6f77f875 | ||
|
|
1f9247c429 | ||
|
|
d089966249 | ||
|
|
b2d629e6a1 | ||
|
|
032087b611 | ||
|
|
22dd2bf75c | ||
|
|
7adc7f43cc | ||
|
|
a38f49cb35 | ||
|
|
ca7684c944 | ||
|
|
955369ab13 | ||
|
|
48f6c0705b | ||
|
|
43eec0e387 | ||
|
|
b1f9609cd3 | ||
|
|
4f731b67d1 | ||
|
|
6549b0fc57 | ||
|
|
ffb972f7c6 | ||
|
|
6ed2daa386 | ||
|
|
8a4042913b | ||
|
|
fb3c2f3bb2 | ||
|
|
5dc4ba504c | ||
|
|
c31dc75c63 | ||
|
|
1f0be929d7 | ||
|
|
56ea1f92fa | ||
|
|
5042d4d747 | ||
|
|
144406ae0e | ||
|
|
3d51f758f8 | ||
|
|
107f6fdfce | ||
|
|
e4d499b550 | ||
|
|
9c3726bdb1 | ||
|
|
a4534b1611 | ||
|
|
e4cad6ed20 | ||
|
|
5f1cfb9072 | ||
|
|
a6ccc8b0da | ||
|
|
fe139c208a | ||
|
|
64c1e842f9 | ||
|
|
cfd7dec04d | ||
|
|
a00676f5db | ||
|
|
61cf7ab843 | ||
|
|
d76d1c628a | ||
|
|
f6b3f6d2ec | ||
|
|
24863c2527 | ||
|
|
ecd6141739 | ||
|
|
ee44920fa4 | ||
|
|
864cbfcfab | ||
|
|
73541eec49 | ||
|
|
87425efa88 | ||
|
|
4455074493 | ||
|
|
5e23476089 | ||
|
|
1232c0268c | ||
|
|
ed9ee65885 | ||
|
|
bc7fe21d27 | ||
|
|
f7b0a28b1e | ||
|
|
b422a419cd | ||
|
|
663f3abff5 | ||
|
|
3c9ce6f8b5 | ||
|
|
c9a128e439 | ||
|
|
e8f356f5ac | ||
|
|
94d64a91b8 | ||
|
|
fdfe9ea2e1 | ||
|
|
d4d7b06b64 | ||
|
|
56d87ecfcf | ||
|
|
76ef2469e8 | ||
|
|
16bd4b41dc | ||
|
|
0e4b79fd16 | ||
|
|
ad73f11b69 | ||
|
|
bacd65b274 | ||
|
|
1f8c55d581 | ||
|
|
ccdb4e0664 | ||
|
|
c77784b5c1 | ||
|
|
74cf71755b | ||
|
|
a4107c87c0 | ||
|
|
8da2cdf430 | ||
|
|
7e93e29f66 | ||
|
|
b2e11137d4 | ||
|
|
d086d64d5f | ||
|
|
fa970986dc | ||
|
|
4c9e89915e | ||
|
|
97c7fd8073 | ||
|
|
29167de546 | ||
|
|
d6f629d4bb | ||
|
|
b13c40e238 | ||
|
|
170fbea7a4 | ||
|
|
08d2747f1e | ||
|
|
b91112fc7a | ||
|
|
ea6b8e0c02 | ||
|
|
404a1d3e8b | ||
|
|
6f1b88e76d | ||
|
|
6169f88d90 | ||
|
|
6f4a4bb764 | ||
|
|
242ae17d0a | ||
|
|
736979c4dc | ||
|
|
4b775fc29d | ||
|
|
ed78b6b3f5 | ||
|
|
a0494d1759 | ||
|
|
1c0c4e955a | ||
|
|
b639c3632d | ||
|
|
6c93b1b768 | ||
|
|
d05255c15b | ||
|
|
59ef26af1c | ||
|
|
33c6ade8f8 | ||
|
|
8c115b8bb0 | ||
|
|
3271fa1d23 | ||
|
|
b43b065cf2 | ||
|
|
66a4618d09 | ||
|
|
983e3c5cbe | ||
|
|
c02d3e3d22 | ||
|
|
c0900b105b | ||
|
|
b6166a2a7c | ||
|
|
38928abab7 | ||
|
|
849f3c52d7 | ||
|
|
f9e55c8f8d | ||
|
|
993a7965fd | ||
|
|
d4f6462e8a | ||
|
|
8bfde2f6d8 | ||
|
|
b3eea2215d | ||
|
|
4d7bc811c4 | ||
|
|
74ec5ea606 | ||
|
|
dda0266798 | ||
|
|
99d9dbe218 | ||
|
|
89c7f05782 | ||
|
|
d9c36a81c4 | ||
|
|
91747c71f2 | ||
|
|
5a1231a17e | ||
|
|
517c7c97d4 | ||
|
|
45af873a6f | ||
|
|
c01167c9da | ||
|
|
a68b3f49b0 | ||
|
|
e03042c411 | ||
|
|
3065bec6c9 | ||
|
|
dae1d12b6f | ||
|
|
c4846cd977 | ||
|
|
f95c9b76d4 | ||
|
|
fb01392bc3 | ||
|
|
498ee478e7 | ||
|
|
53ff6cc21a | ||
|
|
ba33451957 | ||
|
|
d6e253fe7f | ||
|
|
c32a8a863a | ||
|
|
4ba0f8d958 | ||
|
|
e4e2ed41b4 | ||
|
|
888ba108e0 | ||
|
|
c14eb95dba | ||
|
|
dc0ef93680 | ||
|
|
a2ea3c116d | ||
|
|
4578aad0bc | ||
|
|
57448f100c | ||
|
|
835f88d71e | ||
|
|
291d919b9f | ||
|
|
adac96ee84 | ||
|
|
9010a1668b | ||
|
|
f27608947c | ||
|
|
fb2d42da57 | ||
|
|
2bc1d53b18 | ||
|
|
36d3a50f21 | ||
|
|
9bc6479c92 | ||
|
|
56993d3c00 | ||
|
|
86c6135def | ||
|
|
1bb1015fdf | ||
|
|
ac43b6d78a | ||
|
|
809f16c27e | ||
|
|
7860c41959 | ||
|
|
fc1ee9fb2f | ||
|
|
5bc8f410e7 | ||
|
|
3d9ef8c2ed | ||
|
|
0e53ce3ac0 | ||
|
|
4131e6503b | ||
|
|
0aaf78fc51 | ||
|
|
977b2d9e7c | ||
|
|
e76b2c5497 | ||
|
|
8658e11c1d | ||
|
|
b3e4486699 | ||
|
|
2398961473 | ||
|
|
a57bfeba31 | ||
|
|
2f416a87f0 | ||
|
|
9a6c98c134 | ||
|
|
35ca346246 | ||
|
|
0fd9ac15cd | ||
|
|
ae12d77e29 | ||
|
|
9065257961 | ||
|
|
561b55cb9e | ||
|
|
4f871296ae | ||
|
|
55b74ad38f | ||
|
|
8426e36f46 | ||
|
|
85d94aca01 | ||
|
|
39c7089cbc | ||
|
|
eb072ff88a | ||
|
|
0c4046b993 | ||
|
|
90cd5467fe | ||
|
|
05bfb6fc37 | ||
|
|
966b2410d3 | ||
|
|
8ec1ad7255 | ||
|
|
1efa1f4aa3 | ||
|
|
0a48e5f34f | ||
|
|
ad305b3754 | ||
|
|
78cb7d4c15 | ||
|
|
7b5c97f38a | ||
|
|
59bf98e04c | ||
|
|
7feab63e5b | ||
|
|
5d7e168a57 | ||
|
|
8038b7f6a0 | ||
|
|
2533c52e27 | ||
|
|
cf624f4d65 | ||
|
|
a4c98f1382 | ||
|
|
4768485974 | ||
|
|
9a14a5cc10 | ||
|
|
cbffc1a14c | ||
|
|
25e1c6e759 | ||
|
|
e41c35cb5b | ||
|
|
078e111ecd | ||
|
|
01aeceddf4 | ||
|
|
93a3bc2090 | ||
|
|
28b0536916 | ||
|
|
86734f17c4 | ||
|
|
94293e4c63 | ||
|
|
f06d0f4e1e | ||
|
|
a5fc9d9ca9 | ||
|
|
c85a309aeb | ||
|
|
4cac584409 | ||
|
|
b30d3df15c | ||
|
|
6f69654816 | ||
|
|
8fedd7612d | ||
|
|
c16e6e7423 | ||
|
|
c8a056f332 | ||
|
|
60950fb461 | ||
|
|
a3aba8d0db | ||
|
|
f9a48becce | ||
|
|
3140039ccb | ||
|
|
56fedcf495 | ||
|
|
783e9fb140 | ||
|
|
b69d6f57d4 | ||
|
|
125d844e3b | ||
|
|
f04ac180f0 | ||
|
|
1cab452352 | ||
|
|
f3d1d15b61 | ||
|
|
0915071299 | ||
|
|
b787080715 | ||
|
|
a69a6eda4d | ||
|
|
dd757c2114 | ||
|
|
eedea01679 | ||
|
|
0567da94dd | ||
|
|
de92c989f2 | ||
|
|
507843be21 | ||
|
|
b9c1a8a54f | ||
|
|
35283a6923 | ||
|
|
9ae78eda45 | ||
|
|
cc8a24f445 | ||
|
|
5910c65bcf | ||
|
|
e5aee79d47 | ||
|
|
a249e15c58 | ||
|
|
cdcfe328d2 | ||
|
|
784300f690 | ||
|
|
8ad2bef2f5 | ||
|
|
2bd30947fc | ||
|
|
84e8793a29 | ||
|
|
be1643c5b8 | ||
|
|
b00f058eac | ||
|
|
8bab23cfec | ||
|
|
7b26e38f33 | ||
|
|
9e6bd3be76 | ||
|
|
97b016b21b | ||
|
|
84fdb7c647 | ||
|
|
6d70944fc8 | ||
|
|
fcb4fa1b59 | ||
|
|
e69086f1a6 | ||
|
|
fa22607c2c | ||
|
|
9168eba07b | ||
|
|
5d11e37687 | ||
|
|
4ea903b333 | ||
|
|
8fd805815d | ||
|
|
c055690a9b | ||
|
|
dcf146a097 | ||
|
|
e3f50c0ce2 | ||
|
|
c394368dc5 | ||
|
|
1f9c54438a | ||
|
|
f303f305af | ||
|
|
5f1f3dce4a | ||
|
|
f84889ca13 | ||
|
|
b778a80c79 | ||
|
|
0bf632a4b1 | ||
|
|
321c513682 | ||
|
|
9db6a0d438 | ||
|
|
a9affb5ae4 | ||
|
|
46bc8939b4 | ||
|
|
fe6ecf7daf | ||
|
|
a72b896c5f | ||
|
|
a91d790074 | ||
|
|
ac21deefa4 | ||
|
|
37eefe3663 | ||
|
|
2e082ed8b1 | ||
|
|
c1bec66151 | ||
|
|
d53a404bf1 | ||
|
|
7ed4c209fe | ||
|
|
83205d57d9 | ||
|
|
43bb3bdd0c | ||
|
|
cde3f088d1 | ||
|
|
3e7ebf44f3 | ||
|
|
ac7092943c | ||
|
|
e7bbb7fc00 | ||
|
|
2fda29c185 | ||
|
|
2793863689 | ||
|
|
7fafda4747 | ||
|
|
bb0f1e84ce | ||
|
|
3ceba43802 | ||
|
|
f8ed4f48cf | ||
|
|
d319ab9bfc | ||
|
|
902cdc39e0 | ||
|
|
00d3f81aa1 | ||
|
|
d8c91a942f | ||
|
|
30e1c2d2b3 | ||
|
|
4229721774 | ||
|
|
4a45e73125 | ||
|
|
9e819084af | ||
|
|
f39dd2aa1c | ||
|
|
4f3e0bdb1e | ||
|
|
21383b03c5 | ||
|
|
17944211d5 | ||
|
|
1f919e4469 | ||
|
|
06a11f003b | ||
|
|
807867ef42 | ||
|
|
598bc48957 | ||
|
|
7f34ca4122 | ||
|
|
291cd5130d | ||
|
|
280952aae3 | ||
|
|
3ba6899e69 | ||
|
|
65f73bb1ba | ||
|
|
392f0e14b2 | ||
|
|
1e81a89a1a | ||
|
|
11a13ce589 | ||
|
|
24620210fe | ||
|
|
7b2d490ba7 | ||
|
|
20b29f98a7 | ||
|
|
132dbce3a3 | ||
|
|
ded133d164 | ||
|
|
7548ffc191 | ||
|
|
1599ee5682 | ||
|
|
40b57c2df0 | ||
|
|
c6e56d4264 | ||
|
|
7141a91994 | ||
|
|
742a600e38 | ||
|
|
80a2e69eaa | ||
|
|
a7ce6737ec | ||
|
|
dfd7edc540 | ||
|
|
ac65d19809 | ||
|
|
520da3e915 | ||
|
|
d79011355c | ||
|
|
5859270ad0 | ||
|
|
26dc143b1d | ||
|
|
76a8e644e0 | ||
|
|
8d05cb9f3b | ||
|
|
7f5d70bcc8 | ||
|
|
eea1586772 | ||
|
|
6740959866 | ||
|
|
f9c1fa78aa | ||
|
|
f385b24e8c | ||
|
|
508c1407be | ||
|
|
b908dc0ed2 | ||
|
|
6c041fb27f | ||
|
|
63a545736c | ||
|
|
91b355689c | ||
|
|
3e598cf1cd | ||
|
|
9781005a21 | ||
|
|
468272d4c9 | ||
|
|
d5e83aa9de | ||
|
|
69a5f0c2c0 | ||
|
|
f9194dd741 | ||
|
|
fff7cbde22 | ||
|
|
b796a00374 | ||
|
|
cb7b1d92c6 | ||
|
|
fac72b257c | ||
|
|
e6a1bc6e27 | ||
|
|
65794b52ec | ||
|
|
9a4317739b | ||
|
|
de32b86f7c | ||
|
|
5a1faa0fd4 | ||
|
|
57d912efc8 | ||
|
|
87067f7062 | ||
|
|
210bbac583 | ||
|
|
81d3bad747 | ||
|
|
2ddb14a95f | ||
|
|
934c8c61b3 | ||
|
|
459bb59dd5 | ||
|
|
f1c9ed9caa | ||
|
|
5d950b0a5e | ||
|
|
6f78079bc5 | ||
|
|
e3d62388f7 | ||
|
|
5fef9cfe6b | ||
|
|
4a4bec5aec | ||
|
|
4193d3c87c | ||
|
|
0fd83498ea | ||
|
|
00c94755c5 | ||
|
|
e8c2042290 | ||
|
|
6bcb85137b | ||
|
|
d910f30ed1 | ||
|
|
3e8a87c6d6 | ||
|
|
ecb7a9d448 | ||
|
|
40edc38756 | ||
|
|
d1f5d301c2 | ||
|
|
102aca0fa0 | ||
|
|
b0917f5a25 | ||
|
|
5488063490 | ||
|
|
ad125d7af9 | ||
|
|
4510762a35 | ||
|
|
c7ee627110 | ||
|
|
330eac08cb | ||
|
|
bb1d56121d | ||
|
|
3683d3c29b | ||
|
|
0736862a2c | ||
|
|
3891c7008a | ||
|
|
5c729b25b4 | ||
|
|
3151b1634c | ||
|
|
2498f0273d | ||
|
|
40579e1b80 | ||
|
|
2bd6d23467 | ||
|
|
eb7401d693 | ||
|
|
0f5bbb961d | ||
|
|
fa82dea4d5 | ||
|
|
46ef2b6e53 | ||
|
|
4a799b755f | ||
|
|
dda031e73b | ||
|
|
2f8472f720 | ||
|
|
4520ed3cbf | ||
|
|
1ecc3d9744 | ||
|
|
fcf627c30b | ||
|
|
fdf67ab512 | ||
|
|
b1daf2e8bc | ||
|
|
6ecbdda121 | ||
|
|
3f0374e1f2 | ||
|
|
cb345c2364 | ||
|
|
d0b957e998 | ||
|
|
7a2fa4a773 | ||
|
|
8a198fd707 | ||
|
|
7ba3870c82 | ||
|
|
ff0c83a04c | ||
|
|
9a8046b99f | ||
|
|
92b37df962 | ||
|
|
ab4359b624 | ||
|
|
ab5b877dc3 | ||
|
|
23a41ff3c6 | ||
|
|
68a44b6ef7 | ||
|
|
51ea837cd0 | ||
|
|
6f2d5c2752 | ||
|
|
d912c2a090 | ||
|
|
21876857fc | ||
|
|
53405c13af | ||
|
|
abb5f385d9 | ||
|
|
4ad851fdd2 | ||
|
|
8509845381 | ||
|
|
d7eea7fdae | ||
|
|
8395b2640e | ||
|
|
1eae0eb3d4 | ||
|
|
91ffa4a9fd | ||
|
|
58b93c9d22 | ||
|
|
6d2f4d51a2 | ||
|
|
7b63b6900d | ||
|
|
1e52e7ca40 | ||
|
|
2ebdc74f15 | ||
|
|
724e55c37d | ||
|
|
51f1923e22 | ||
|
|
714f6c058f | ||
|
|
560f601190 | ||
|
|
6deb039906 | ||
|
|
f19eaf689b | ||
|
|
80f6570f04 | ||
|
|
d883096971 | ||
|
|
87f9afbd85 | ||
|
|
9bb5241e49 | ||
|
|
65601cb855 | ||
|
|
ef86570b24 | ||
|
|
821c262a93 | ||
|
|
1c323675d1 | ||
|
|
2c9e675ba4 | ||
|
|
2d5bebb969 | ||
|
|
a97913fd63 | ||
|
|
9264306a36 | ||
|
|
d2ac174427 | ||
|
|
d5a8a0d72f | ||
|
|
36bfbe10ab | ||
|
|
7ace02dd46 | ||
|
|
125a3ace08 | ||
|
|
3c7d03ada9 | ||
|
|
477d38d928 | ||
|
|
d36bcb1d4d | ||
|
|
4c79999a65 | ||
|
|
cdfed0fe94 | ||
|
|
6af915983c | ||
|
|
da266792df | ||
|
|
91afdf7f13 | ||
|
|
26fc6098dc | ||
|
|
3496169c68 | ||
|
|
299add4a15 | ||
|
|
5ab76c98e5 | ||
|
|
f5b4984295 | ||
|
|
a38665fa0d | ||
|
|
cf27ff10c0 | ||
|
|
8f3f520ef4 | ||
|
|
c4e4f78336 | ||
|
|
2f9eb28596 | ||
|
|
63e90a5c17 | ||
|
|
61d13a6cab | ||
|
|
a2ecc67643 | ||
|
|
f679999453 | ||
|
|
5b8d7dbff5 | ||
|
|
9bbdf5f6f6 | ||
|
|
812ddf2ebb | ||
|
|
db3ea7ed73 | ||
|
|
c37ef867a1 | ||
|
|
7c6c908076 | ||
|
|
c510afdc28 | ||
|
|
861e207fb6 | ||
|
|
c601e45436 | ||
|
|
e79c163dd9 | ||
|
|
c770b97649 | ||
|
|
5dedf5c1b5 | ||
|
|
85bd0ed2f8 | ||
|
|
cd6a183c28 | ||
|
|
3cc8c8fb03 | ||
|
|
42408572ab | ||
|
|
c3956c5894 | ||
|
|
b2e9058a2f | ||
|
|
bc28b11763 | ||
|
|
cbd71bec49 | ||
|
|
6ac172fe02 | ||
|
|
8ebcfa4bc6 | ||
|
|
3f4cec1719 | ||
|
|
156146fd9a | ||
|
|
e86e7344f3 | ||
|
|
8d9f206c45 | ||
|
|
39d8d8bcfa | ||
|
|
a719db4d0d | ||
|
|
a845067cf0 | ||
|
|
82d71d65fa | ||
|
|
a699cfb958 | ||
|
|
92b24c6eb2 | ||
|
|
d57092feae | ||
|
|
6c4b495a75 | ||
|
|
d0b7ccf302 | ||
|
|
e237bd04ff | ||
|
|
2a686b55c4 | ||
|
|
cdc3b18071 | ||
|
|
c8860a3a9d | ||
|
|
57a67bf4df | ||
|
|
f932d580af | ||
|
|
eadcb3f22b | ||
|
|
a6d722f9a9 | ||
|
|
f10280c8bb | ||
|
|
a6848be4c2 | ||
|
|
f510c1922d | ||
|
|
0562dbbbf9 | ||
|
|
85b92d9c6f | ||
|
|
de465ebcba | ||
|
|
8302285388 | ||
|
|
b502161b11 | ||
|
|
1206be34dc | ||
|
|
c6cf5a0fab | ||
|
|
d6df496216 | ||
|
|
68874e8680 | ||
|
|
67f0c482b3 | ||
|
|
1ceec16102 | ||
|
|
9d03006aaa | ||
|
|
f7f21f9716 | ||
|
|
124d9becc6 | ||
|
|
50777ef32f | ||
|
|
563a151277 | ||
|
|
6f7528c87a | ||
|
|
ae0228dc25 | ||
|
|
a1f87c50bc | ||
|
|
74e65d75cb | ||
|
|
56967d4c0c | ||
|
|
2950862e34 | ||
|
|
f4ecdd5af3 | ||
|
|
dd456edf90 | ||
|
|
e1f1addb35 | ||
|
|
94e59592f0 | ||
|
|
4cd94f0426 | ||
|
|
c99f470e34 | ||
|
|
45b0fbeb4a | ||
|
|
76f0368a64 | ||
|
|
da80f47921 | ||
|
|
fa7c19d5de | ||
|
|
74f54b3d76 | ||
|
|
2f49643e51 | ||
|
|
fabdf67da7 | ||
|
|
fc71f61000 | ||
|
|
aa8a72a9d8 | ||
|
|
a1dcaa2683 | ||
|
|
f533e2a547 | ||
|
|
496ca05b9d | ||
|
|
f399463a99 | ||
|
|
44c98553dc | ||
|
|
57a8f5f0b3 | ||
|
|
ab7c5678c6 | ||
|
|
c323985f03 | ||
|
|
024f35496c | ||
|
|
4d91096ab9 | ||
|
|
8148c0fa29 | ||
|
|
620b3e3abc | ||
|
|
22af8e91cc | ||
|
|
9dcefa4357 | ||
|
|
634d78456d | ||
|
|
8140ddc2ff | ||
|
|
71cfbc8c0a | ||
|
|
6a9dee38ef | ||
|
|
9ee31e3a6a | ||
|
|
7379bcb5b6 | ||
|
|
246c475dbe | ||
|
|
864631f967 | ||
|
|
4fe5681917 | ||
|
|
fe8e5a0464 | ||
|
|
7c914b88a3 | ||
|
|
256cd06e5c | ||
|
|
f3ae0101d7 | ||
|
|
52efc632f8 | ||
|
|
f3f0f611cb | ||
|
|
07fcd29842 | ||
|
|
863107670c | ||
|
|
c2ca05b117 | ||
|
|
3c39ea192b | ||
|
|
1533b2d3a1 | ||
|
|
7bcb227d7b | ||
|
|
178ad2ac8a | ||
|
|
bdd981e15c | ||
|
|
a61526543d | ||
|
|
1ab3463e6d | ||
|
|
269b2765cd | ||
|
|
d2563db5a0 | ||
|
|
fcedb65119 | ||
|
|
18b79913bd | ||
|
|
9fb4aff635 | ||
|
|
38efdc8f36 | ||
|
|
75700e3309 | ||
|
|
48e57a2122 | ||
|
|
d791705afa | ||
|
|
38e3d9909f | ||
|
|
2a234f5a88 | ||
|
|
5f00266df7 | ||
|
|
c917d7dccb | ||
|
|
54c39ab8a3 | ||
|
|
b19fb316d9 | ||
|
|
7a849806fb | ||
|
|
01ccb771e6 | ||
|
|
c6683712a4 | ||
|
|
d2b202c25f | ||
|
|
dfbefcd9d2 | ||
|
|
938c4b2d25 | ||
|
|
81182aa65b | ||
|
|
487158739d | ||
|
|
7899b124b7 | ||
|
|
e1d623be9c |
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug from noctalia-shell
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Description
|
||||
A clear and concise description of the bug.
|
||||
|
||||
### Steps to Reproduce
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. See the error.
|
||||
|
||||
### Expected Behavior
|
||||
Explain what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
Add screenshots if applicable.
|
||||
|
||||
### Environment
|
||||
- Distro: [e.g., CachyOS, NixOS, Arch, ...]
|
||||
- Compositor: [ e.g., Hyprland, Niri, ...]
|
||||
- Noctalia-shell Version: [e.g., 1.0.0, available in About tab]
|
||||
|
||||
### Additional Context
|
||||
Add any other context about the problem here.
|
||||
12
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
blank_issues_enabled: false
|
||||
issue_templates:
|
||||
- name: "Bug Report"
|
||||
description: "Report a bug in the system."
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body: "./ISSUE_TEMPLATE/bug_report.md"
|
||||
- name: "Feature Request"
|
||||
description: "Propose a new feature or improvement."
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body: "./ISSUE_TEMPLATE/feature_request.md"
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature or improvement
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Feature Description
|
||||
What feature would you like to see?
|
||||
|
||||
### Why Is This Needed?
|
||||
Explain the problem or need for this feature.
|
||||
|
||||
### Suggested Solutions
|
||||
Describe how this feature could be implemented.
|
||||
|
||||
### Additional Context
|
||||
Add any relevant screenshots, links, or resources.
|
||||
110
.github/workflows/update-aur-package.yml
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
name: Update AUR Package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
aur-sync:
|
||||
name: Sync PKGBUILD with release
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux:latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
AUR_REPO: ssh://aur@aur.archlinux.org/noctalia-shell.git
|
||||
GIT_SSH_COMMAND: ssh -i /root/.ssh/id_aur -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes
|
||||
PKGNAME: noctalia-shell
|
||||
AUR_LINK: https://aur.archlinux.org/packages/noctalia-shell
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pacman -Syu --noconfirm git base-devel pacman-contrib openssh
|
||||
|
||||
- name: Create build user
|
||||
run: |
|
||||
set -euo pipefail
|
||||
useradd -m builduser
|
||||
echo 'builduser ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p /root/.ssh
|
||||
chmod 700 /root/.ssh
|
||||
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > /root/.ssh/id_aur
|
||||
chmod 600 /root/.ssh/id_aur
|
||||
ssh-keyscan aur.archlinux.org >> /root/.ssh/known_hosts
|
||||
chmod 600 /root/.ssh/known_hosts
|
||||
|
||||
- name: Determine version
|
||||
id: vars
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKGVER="${TAG_NAME#v}"
|
||||
echo "pkgver=$PKGVER" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Clone AUR repository
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone "$AUR_REPO" "$GITHUB_WORKSPACE/aur"
|
||||
|
||||
- name: Update PKGBUILD
|
||||
env:
|
||||
PKGVER: ${{ steps.vars.outputs.pkgver }}
|
||||
working-directory: ${{ github.workspace }}/aur
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" PKGBUILD
|
||||
sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD
|
||||
|
||||
- name: Refresh checksums and metadata
|
||||
env:
|
||||
AUR_DIR: ${{ github.workspace }}/aur
|
||||
run: |
|
||||
set -euo pipefail
|
||||
chown -R builduser:builduser "$AUR_DIR"
|
||||
su - builduser -c "cd $AUR_DIR && updpkgsums"
|
||||
su - builduser -c "cd $AUR_DIR && makepkg --printsrcinfo > .SRCINFO"
|
||||
|
||||
- name: Commit and push changes
|
||||
env:
|
||||
PKGVER: ${{ steps.vars.outputs.pkgver }}
|
||||
working-directory: ${{ github.workspace }}/aur
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory "$PWD"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
git add PKGBUILD .SRCINFO
|
||||
git commit -m "chore(package): release ${PKGVER}"
|
||||
git push origin HEAD
|
||||
else
|
||||
echo "No updates necessary."
|
||||
fi
|
||||
|
||||
- name: Summarize update
|
||||
env:
|
||||
PKGNAME: noctalia-shell
|
||||
PKGVER: ${{ steps.vars.outputs.pkgver }}
|
||||
AUR_LINK: https://aur.archlinux.org/packages/noctalia-shell
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo "## AUR Update"
|
||||
echo ""
|
||||
echo "- Package: ${PKGNAME}"
|
||||
echo "- Updated version: ${PKGVER}"
|
||||
echo "- AUR page: ${AUR_LINK}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
0
.gitignore
vendored
Normal file
34
Assets/ColorScheme/Ayu.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#E6B450",
|
||||
"mOnPrimary": "#0B0E14",
|
||||
"mSecondary": "#AAD94C",
|
||||
"mOnSecondary": "#0B0E14",
|
||||
"mTertiary": "#39BAE6",
|
||||
"mOnTertiary": "#0B0E14",
|
||||
"mError": "#D95757",
|
||||
"mOnError": "#0B0E14",
|
||||
"mSurface": "#1e222a",
|
||||
"mOnSurface": "#BFBDB6",
|
||||
"mSurfaceVariant": "#0B0E14",
|
||||
"mOnSurfaceVariant": "#636A72",
|
||||
"mOutline": "#565B66",
|
||||
"mShadow": "#000000"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#FF8F40",
|
||||
"mOnPrimary": "#F8F9FA",
|
||||
"mSecondary": "#86B300",
|
||||
"mOnSecondary": "#F8F9FA",
|
||||
"mTertiary": "#55B4D4",
|
||||
"mOnTertiary": "#F8F9FA",
|
||||
"mError": "#E65050",
|
||||
"mOnError": "#F8F9FA",
|
||||
"mSurface": "#E4E6E9",
|
||||
"mOnSurface": "#5C6166",
|
||||
"mSurfaceVariant": "#F8F9FA",
|
||||
"mOnSurfaceVariant": "#ABADB1",
|
||||
"mOutline": "#8A9199",
|
||||
"mShadow": "#F8F9FA"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"mOnPrimary": "#11111b",
|
||||
"mSecondary": "#fab387",
|
||||
"mOnSecondary": "#11111b",
|
||||
"mTertiary": "#a6e3a1",
|
||||
"mTertiary": "#94e2d5",
|
||||
"mOnTertiary": "#11111b",
|
||||
"mError": "#f38ba8",
|
||||
"mOnError": "#11111b",
|
||||
@@ -16,19 +16,19 @@
|
||||
"mShadow": "#11111b"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#9349ef",
|
||||
"mPrimary": "#8839ef",
|
||||
"mOnPrimary": "#eff1f5",
|
||||
"mSecondary": "#f67525",
|
||||
"mSecondary": "#fe640b",
|
||||
"mOnSecondary": "#eff1f5",
|
||||
"mTertiary": "#40b635",
|
||||
"mTertiary": "#40a02b",
|
||||
"mOnTertiary": "#eff1f5",
|
||||
"mError": "#f38ba8",
|
||||
"mOnError": "#11111b",
|
||||
"mError": "#d20f39",
|
||||
"mOnError": "#dce0e8",
|
||||
"mSurface": "#eff1f5",
|
||||
"mOnSurface": "#4c4f69",
|
||||
"mSurfaceVariant": "#ccd0da",
|
||||
"mOnSurfaceVariant": "#6c6f85",
|
||||
"mOutline": "#aeb5c4",
|
||||
"mOutline": "#a5adcb",
|
||||
"mShadow": "#dce0e8"
|
||||
}
|
||||
}
|
||||
|
||||
34
Assets/ColorScheme/Everforest.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#D3C6AA",
|
||||
"mOnPrimary": "#232A2E",
|
||||
"mSecondary": "#D3C6AA",
|
||||
"mOnSecondary": "#232A2E",
|
||||
"mTertiary": "#9DA9A0",
|
||||
"mOnTertiary": "#232A2E",
|
||||
"mError": "#E67E80",
|
||||
"mOnError": "#232A2E",
|
||||
"mSurface": "#232A2E",
|
||||
"mOnSurface": "#859289",
|
||||
"mSurfaceVariant": "#2D353B",
|
||||
"mOnSurfaceVariant": "#D3C6AA",
|
||||
"mOutline": "#D3C6AA",
|
||||
"mShadow": "#475258"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#434F55",
|
||||
"mOnPrimary": "#D3C6AA",
|
||||
"mSecondary": "#232a2e",
|
||||
"mOnSecondary": "#D3C6AA",
|
||||
"mTertiary": "#333c43",
|
||||
"mOnTertiary": "#9DA9A0",
|
||||
"mError": "#E66868",
|
||||
"mOnError": "#9DA9A0",
|
||||
"mSurface": "#BEC5B2",
|
||||
"mOnSurface": "#333C43",
|
||||
"mSurfaceVariant": "#9DA9A0",
|
||||
"mOnSurfaceVariant": "#232A2E",
|
||||
"mOutline": "#232A2E",
|
||||
"mShadow": "#ECF5ED"
|
||||
}
|
||||
}
|
||||
34
Assets/ColorScheme/Kanagawa.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#76946a",
|
||||
"mOnPrimary": "#1f1f28",
|
||||
"mSecondary": "#c0a36e",
|
||||
"mOnSecondary": "#1f1f28",
|
||||
"mTertiary": "#7e9cd8",
|
||||
"mOnTertiary": "#1f1f28",
|
||||
"mError": "#c34043",
|
||||
"mOnError": "#1f1f28",
|
||||
"mSurface": "#1f1f28",
|
||||
"mOnSurface": "#717c7c",
|
||||
"mSurfaceVariant": "#2a2a37",
|
||||
"mOnSurfaceVariant": "#c8c093",
|
||||
"mOutline": "#363646",
|
||||
"mShadow": "#1f1f28"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#6f894e",
|
||||
"mOnPrimary": "#f2ecbc",
|
||||
"mSecondary": "#77713f",
|
||||
"mOnSecondary": "#f2ecbc",
|
||||
"mTertiary": "#4d699b",
|
||||
"mOnTertiary": "#f2ecbc",
|
||||
"mError": "#c84053",
|
||||
"mOnError": "#f2ecbc",
|
||||
"mSurface": "#f2ecbc",
|
||||
"mOnSurface": "#8a8980",
|
||||
"mSurfaceVariant": "#e5ddb0",
|
||||
"mOnSurfaceVariant": "#545464",
|
||||
"mOutline": "#cfc49c",
|
||||
"mShadow": "#f2ecbc"
|
||||
}
|
||||
}
|
||||
34
Assets/ColorScheme/Monochrome.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#aaaaaa",
|
||||
"mOnPrimary": "#111111",
|
||||
"mSecondary": "#a7a7a7",
|
||||
"mOnSecondary": "#111111",
|
||||
"mTertiary": "#cccccc",
|
||||
"mOnTertiary": "#111111",
|
||||
"mError": "#dddddd",
|
||||
"mOnError": "#111111",
|
||||
"mSurface": "#111111",
|
||||
"mOnSurface": "#828282",
|
||||
"mSurfaceVariant": "#191919",
|
||||
"mOnSurfaceVariant": "#5d5d5d",
|
||||
"mOutline": "#3c3c3c",
|
||||
"mShadow": "#000000"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#555555",
|
||||
"mOnPrimary": "#eeeeee",
|
||||
"mSecondary": "#505058",
|
||||
"mOnSecondary": "#eeeeee",
|
||||
"mTertiary": "#333333",
|
||||
"mOnTertiary": "#eeeeee",
|
||||
"mError": "#222222",
|
||||
"mOnError": "#efefef",
|
||||
"mSurface": "#d4d4d4",
|
||||
"mOnSurface": "#696969",
|
||||
"mSurfaceVariant": "#e8e8e8",
|
||||
"mOnSurfaceVariant": "#9e9e9e",
|
||||
"mOutline": "#c3c3c3",
|
||||
"mShadow": "#fafafa"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#c7a1d8",
|
||||
"mOnPrimary": "#1a151f",
|
||||
"mSecondary": "#a984c4",
|
||||
"mOnSecondary": "#f3edf7",
|
||||
"mTertiary": "#e0b7c9",
|
||||
"mOnTertiary": "#20161f",
|
||||
"mError": "#e9899d",
|
||||
"mOnError": "#1e1418",
|
||||
"mSurface": "#1c1822",
|
||||
"mOnSurface": "#e9e4f0",
|
||||
"mSurfaceVariant": "#262130",
|
||||
"mOnSurfaceVariant": "#a79ab0",
|
||||
"mOutline": "#3e364e",
|
||||
"mShadow": "#120f18"
|
||||
"mPrimary": "#fff59b",
|
||||
"mOnPrimary": "#0e0e43",
|
||||
"mSecondary": "#a9aefe",
|
||||
"mOnSecondary": "#0e0e43",
|
||||
"mTertiary": "#9BFECE",
|
||||
"mOnTertiary": "#0e0e43",
|
||||
"mError": "#FD4663",
|
||||
"mOnError": "#0e0e43",
|
||||
"mSurface": "#070722",
|
||||
"mOnSurface": "#f3edf7",
|
||||
"mSurfaceVariant": "#11112d",
|
||||
"mOnSurfaceVariant": "#7c80b4",
|
||||
"mOutline": "#21215F",
|
||||
"mShadow": "#070722"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#9b59ba",
|
||||
"mOnPrimary": "#ffffff",
|
||||
"mSecondary": "#784999",
|
||||
"mOnSecondary": "#ffffff",
|
||||
"mTertiary": "#c17093",
|
||||
"mOnTertiary": "#ffffff",
|
||||
"mError": "#e9899d",
|
||||
"mOnError": "#1e1418",
|
||||
"mSurface": "#f5f1fa",
|
||||
"mOnSurface": "#1c1822",
|
||||
"mSurfaceVariant": "#e7dfee",
|
||||
"mOnSurfaceVariant": "#4a3d59",
|
||||
"mOutline": "#cebedc",
|
||||
"mShadow": "#ffffff"
|
||||
"mPrimary": "#5d65f5",
|
||||
"mOnPrimary": "#dadcff",
|
||||
"mSecondary": "#8E93D8",
|
||||
"mOnSecondary": "#dadcff",
|
||||
"mTertiary": "#0e0e43",
|
||||
"mOnTertiary": "#fef29a",
|
||||
"mError": "#FD4663",
|
||||
"mOnError": "#0e0e43",
|
||||
"mSurface": "#e6e8fa",
|
||||
"mOnSurface": "#4b55c8",
|
||||
"mSurfaceVariant": "#eff0ff",
|
||||
"mOnSurfaceVariant": "#0e0e43",
|
||||
"mOutline": "#8288fc",
|
||||
"mShadow": "#f3edf7"
|
||||
}
|
||||
}
|
||||
|
||||
34
Assets/ColorScheme/Noctalia (legacy).json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#c7a1d8",
|
||||
"mOnPrimary": "#1a151f",
|
||||
"mSecondary": "#a984c4",
|
||||
"mOnSecondary": "#f3edf7",
|
||||
"mTertiary": "#e0b7c9",
|
||||
"mOnTertiary": "#20161f",
|
||||
"mError": "#e9899d",
|
||||
"mOnError": "#1e1418",
|
||||
"mSurface": "#1c1822",
|
||||
"mOnSurface": "#e9e4f0",
|
||||
"mSurfaceVariant": "#262130",
|
||||
"mOnSurfaceVariant": "#a79ab0",
|
||||
"mOutline": "#3e364e",
|
||||
"mShadow": "#120f18"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#9b59ba",
|
||||
"mOnPrimary": "#ffffff",
|
||||
"mSecondary": "#784999",
|
||||
"mOnSecondary": "#ffffff",
|
||||
"mTertiary": "#c17093",
|
||||
"mOnTertiary": "#ffffff",
|
||||
"mError": "#e9899d",
|
||||
"mOnError": "#1e1418",
|
||||
"mSurface": "#f5f1fa",
|
||||
"mOnSurface": "#1c1822",
|
||||
"mSurfaceVariant": "#e7dfee",
|
||||
"mOnSurfaceVariant": "#4a3d59",
|
||||
"mOutline": "#cebedc",
|
||||
"mShadow": "#ffffff"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#ebbcba",
|
||||
"mOnPrimary": "#1f1d2e",
|
||||
"mOnPrimary": "#191724",
|
||||
"mSecondary": "#9ccfd8",
|
||||
"mOnSecondary": "#1f1d2e",
|
||||
"mTertiary": "#f6c177",
|
||||
"mOnTertiary": "#1f1d2e",
|
||||
"mOnSecondary": "#191724",
|
||||
"mTertiary": "#524f67",
|
||||
"mOnTertiary": "#e0def4",
|
||||
"mError": "#eb6f92",
|
||||
"mOnError": "#1f1d2e",
|
||||
"mSurface": "#1f1d2e",
|
||||
"mOnError": "#191724",
|
||||
"mSurface": "#191724",
|
||||
"mOnSurface": "#e0def4",
|
||||
"mSurfaceVariant": "#26233a",
|
||||
"mOnSurfaceVariant": "#908caa",
|
||||
"mOutline": "#403d52",
|
||||
"mShadow": "#1f1d2e"
|
||||
"mShadow": "#191724"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#d46e6b",
|
||||
"mPrimary": "#d7827e",
|
||||
"mOnPrimary": "#faf4ed",
|
||||
"mSecondary": "#56949f",
|
||||
"mOnSecondary": "#faf4ed",
|
||||
"mTertiary": "#31748f",
|
||||
"mOnTertiary": "#232136",
|
||||
"mTertiary": "#cecacd",
|
||||
"mOnTertiary": "#575279",
|
||||
"mError": "#b4637a",
|
||||
"mOnError": "#f2e9e1",
|
||||
"mSurface": "#e0def4",
|
||||
"mOnSurface": "#232136",
|
||||
"mSurfaceVariant": "#bcb8e7",
|
||||
"mOnError": "#faf4ed",
|
||||
"mSurface": "#faf4ed",
|
||||
"mOnSurface": "#575279",
|
||||
"mSurfaceVariant": "#f2e9e1",
|
||||
"mOnSurfaceVariant": "#797593",
|
||||
"mOutline": "#9893a5",
|
||||
"mShadow": "#575279"
|
||||
"mOutline": "#dfdad9",
|
||||
"mShadow": "#faf4ed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#ff9e64",
|
||||
"mOnPrimary": "#1a1b26",
|
||||
"mSecondary": "#e0af68",
|
||||
"mOnSecondary": "#1a1b26",
|
||||
"mTertiary": "#7aa2f7",
|
||||
"mOnTertiary": "#1a1b26",
|
||||
"mPrimary": "#7aa2f7",
|
||||
"mOnPrimary": "#16161e",
|
||||
"mSecondary": "#bb9af7",
|
||||
"mOnSecondary": "#16161e",
|
||||
"mTertiary": "#9ece6a",
|
||||
"mOnTertiary": "#16161e",
|
||||
"mError": "#f7768e",
|
||||
"mOnError": "#1a1b26",
|
||||
"mOnError": "#16161e",
|
||||
"mSurface": "#1a1b26",
|
||||
"mOnSurface": "#a9b1d6",
|
||||
"mSurfaceVariant": "#292e42",
|
||||
"mOnSurfaceVariant": "#787c99",
|
||||
"mOutline": "#3d4462",
|
||||
"mShadow": "#1a1b26"
|
||||
"mOnSurface": "#c0caf5",
|
||||
"mSurfaceVariant": "#24283b",
|
||||
"mOnSurfaceVariant": "#9aa5ce",
|
||||
"mOutline": "#565f89",
|
||||
"mShadow": "#15161e"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#fd5d00",
|
||||
"mOnPrimary": "#e6e7ed",
|
||||
"mSecondary": "#bb8027",
|
||||
"mOnSecondary": "#e6e7ed",
|
||||
"mTertiary": "#4a80f4",
|
||||
"mOnTertiary": "#e6e7ed",
|
||||
"mError": "#965027",
|
||||
"mOnError": "#e6e7ed",
|
||||
"mSurface": "#e6e7ed",
|
||||
"mOnSurface": "#343b58",
|
||||
"mSurfaceVariant": "#d5d6db",
|
||||
"mOnSurfaceVariant": "#40434f",
|
||||
"mOutline": "#babbc3",
|
||||
"mShadow": "#c0caf5"
|
||||
"mPrimary": "#2e7de9",
|
||||
"mOnPrimary": "#e1e2e7",
|
||||
"mSecondary": "#9854f1",
|
||||
"mOnSecondary": "#e1e2e7",
|
||||
"mTertiary": "#587539",
|
||||
"mOnTertiary": "#e1e2e7",
|
||||
"mError": "#f52a65",
|
||||
"mOnError": "#e1e2e7",
|
||||
"mSurface": "#e1e2e7",
|
||||
"mOnSurface": "#3760bf",
|
||||
"mSurfaceVariant": "#d0d5e3",
|
||||
"mOnSurfaceVariant": "#6172b0",
|
||||
"mOutline": "#b4b5b9",
|
||||
"mShadow": "#a8aecb"
|
||||
}
|
||||
}
|
||||
|
||||
16
Assets/Fonts/tabler/tabler-icons-license.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Tabler Licenses - Detailed Usage Rights and Guidelines
|
||||
|
||||
This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Admin Template, Tabler Icons, Tabler Emails, Tabler Illustrations), constitutes your acceptance of the terms of this license, Tabler terms of service and Tabler private policy.
|
||||
|
||||
Tabler Admin Template and Tabler Icons License*
|
||||
Tabler Admin Template and Tabler Icons are available under MIT License.
|
||||
|
||||
Copyright (c) 2018-2025 Tabler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
See more at Tabler Admin Template MIT License See more at Tabler Icons MIT License
|
||||
BIN
Assets/Fonts/tabler/tabler-icons.ttf
Normal file
84
Assets/Matugen/Matugen.qml
Normal file
@@ -0,0 +1,84 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
// Central place to define which templates we generate and where they write.
|
||||
// Users can extend it by dropping additional templates into:
|
||||
// - Assets/Matugen/templates/
|
||||
// - ~/.config/matugen/ (when enableUserTemplates is true)
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Build the base TOML using current settings
|
||||
function buildConfigToml() {
|
||||
var lines = []
|
||||
lines.push("[config]")
|
||||
var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
|
||||
// Always include noctalia colors output for the shell
|
||||
lines.push("[templates.noctalia]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/noctalia.json"')
|
||||
lines.push('output_path = "' + Settings.configDir + 'colors.json"')
|
||||
|
||||
if (Settings.data.matugen.gtk4) {
|
||||
lines.push("\n[templates.gtk4]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/gtk4.css"')
|
||||
lines.push('output_path = "~/.config/gtk-4.0/gtk.css"')
|
||||
lines.push("post_hook = 'gsettings set org.gnome.desktop.interface color-scheme prefer-" + mode + "'")
|
||||
}
|
||||
if (Settings.data.matugen.gtk3) {
|
||||
lines.push("\n[templates.gtk3]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/gtk3.css"')
|
||||
lines.push('output_path = "~/.config/gtk-3.0/gtk.css"')
|
||||
lines.push("post_hook = 'gsettings set org.gnome.desktop.interface color-scheme prefer-" + mode + "'")
|
||||
}
|
||||
if (Settings.data.matugen.qt6) {
|
||||
lines.push("\n[templates.qt6]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/qtct.conf"')
|
||||
lines.push('output_path = "~/.config/qt6ct/colors/noctalia.conf"')
|
||||
}
|
||||
if (Settings.data.matugen.qt5) {
|
||||
lines.push("\n[templates.qt5]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/qtct.conf"')
|
||||
lines.push('output_path = "~/.config/qt5ct/colors/noctalia.conf"')
|
||||
}
|
||||
if (Settings.data.matugen.kitty) {
|
||||
lines.push("\n[templates.kitty]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/kitty.conf"')
|
||||
lines.push('output_path = "~/.config/kitty/themes/noctalia.conf"')
|
||||
lines.push("post_hook = 'kitty +kitten themes --reload-in=all noctalia'")
|
||||
}
|
||||
if (Settings.data.matugen.ghostty) {
|
||||
lines.push("\n[templates.ghostty]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/ghostty.conf"')
|
||||
lines.push('output_path = "~/.config/ghostty/themes/noctalia"')
|
||||
lines.push("post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config; and pkill -SIGUSR2 ghostty\"")
|
||||
}
|
||||
if (Settings.data.matugen.foot) {
|
||||
lines.push("\n[templates.foot]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/foot.conf"')
|
||||
lines.push('output_path = "~/.config/foot/themes/noctalia"')
|
||||
lines.push('post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"')
|
||||
}
|
||||
if (Settings.data.matugen.fuzzel) {
|
||||
lines.push("\n[templates.fuzzel]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/fuzzel.conf"')
|
||||
lines.push('output_path = "~/.config/fuzzel/themes/noctalia"')
|
||||
lines.push('post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"')
|
||||
}
|
||||
if (Settings.data.matugen.vesktop) {
|
||||
lines.push("\n[templates.vesktop]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/vesktop.css"')
|
||||
lines.push('output_path = "~/.config/vesktop/themes/noctalia.theme.css"')
|
||||
}
|
||||
if (Settings.data.matugen.pywalfox) {
|
||||
lines.push("\n[templates.pywalfox]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/pywalfox.json"')
|
||||
lines.push('output_path = "~/.cache/wal/colors.json"')
|
||||
lines.push('post_hook = "pywalfox update"')
|
||||
}
|
||||
|
||||
return lines.join("\n") + "\n"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
# Base: only write Noctalia colors.json for the shell
|
||||
[config]
|
||||
|
||||
[templates.noctalia]
|
||||
input_path = "templates/noctalia.json"
|
||||
output_path = "~/.config/noctalia/colors.json"
|
||||
@@ -1,30 +0,0 @@
|
||||
# This file configures how matugen generates colors from wallpapers for Noctalia
|
||||
[config]
|
||||
|
||||
|
||||
[templates.noctalia]
|
||||
input_path = "templates/noctalia.json"
|
||||
output_path = "~/.config/noctalia/colors.json"
|
||||
|
||||
# GTK 4 (libadwaita) variables override
|
||||
[templates.gtk4]
|
||||
input_path = "templates/gtk4.css"
|
||||
output_path = "~/.config/gtk-4.0/gtk.css"
|
||||
|
||||
# GTK 3 named-colors fallback for legacy apps
|
||||
[templates.gtk3]
|
||||
input_path = "templates/gtk3.css"
|
||||
output_path = "~/.config/gtk-3.0/gtk.css"
|
||||
|
||||
# Qt6ct color scheme (can also be used by qt5ct in many distros)
|
||||
[templates.qt6]
|
||||
input_path = "templates/qtct.conf"
|
||||
output_path = "~/.config/qt6ct/colors/noctalia.conf"
|
||||
|
||||
[templates.qt5]
|
||||
input_path = "templates/qtct.conf"
|
||||
output_path = "~/.config/qt5ct/colors/noctalia.conf"
|
||||
|
||||
[templates.kitty]
|
||||
input_path = "templates/kitty.conf"
|
||||
output_path = "~/.config/kitty/noctalia.conf"
|
||||
30
Assets/Matugen/templates/foot.conf
Normal file
@@ -0,0 +1,30 @@
|
||||
[colors]
|
||||
background={{ colors.background.default.hex_stripped }}
|
||||
foreground={{ colors.on_surface.default.hex_stripped }}
|
||||
regular0={{ colors.surface.default.hex_stripped }}
|
||||
regular1={{ colors.error.default.hex_stripped }}
|
||||
regular2={{ colors.primary.default.hex_stripped }}
|
||||
regular3={{ colors.tertiary.default.hex_stripped }}
|
||||
regular4={{ colors.on_primary_container.default.hex_stripped }}
|
||||
regular5={{ colors.on_secondary_container.default.hex_stripped }}
|
||||
regular6={{ colors.secondary.default.hex_stripped }}
|
||||
regular7={{ colors.on_surface.default.hex_stripped }}
|
||||
bright0={{ colors.surface_bright.default.hex_stripped }}
|
||||
bright1={{ colors.error.default.hex_stripped }}
|
||||
bright2={{ colors.primary.default.hex_stripped }}
|
||||
bright3={{ colors.tertiary.default.hex_stripped }}
|
||||
bright4={{ colors.on_primary_container.default.hex_stripped }}
|
||||
bright5={{ colors.on_secondary_container.default.hex_stripped }}
|
||||
bright6={{ colors.secondary.default.hex_stripped }}
|
||||
bright7={{ colors.on_surface.default.hex_stripped }}
|
||||
dim0=45475A
|
||||
dim1=F38BA8
|
||||
dim2=A6E3A1
|
||||
dim3=F9E2AF
|
||||
dim4=89B4FA
|
||||
dim5=F5C2E7
|
||||
dim6=94E2D5
|
||||
dim7=BAC2DE
|
||||
selection-foreground={{ colors.primary.default.hex_stripped }}
|
||||
selection-background={{ colors.on_primary.default.hex_stripped }}
|
||||
cursor={{ colors.surface_variant.default.hex_stripped }} {{ colors.on_surface.default.hex_stripped }}
|
||||
15
Assets/Matugen/templates/fuzzel.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
# Fuzzel Colors
|
||||
# Generated with Matugen
|
||||
|
||||
[colors]
|
||||
background={{colors.background.default.hex_stripped}}CC
|
||||
text={{colors.on_surface.default.hex_stripped}}ff
|
||||
prompt={{colors.secondary.default.hex_stripped}}ff
|
||||
placeholder={{colors.tertiary.default.hex_stripped}}ff
|
||||
input={{colors.primary.default.hex_stripped}}ff
|
||||
match={{colors.tertiary.default.hex_stripped}}ff
|
||||
selection={{colors.primary.default.hex_stripped}}80
|
||||
selection-text={{colors.on_surface.default.hex_stripped}}ff
|
||||
selection-match={{colors.on_primary.default.hex_stripped}}ff
|
||||
counter={{colors.secondary.default.hex_stripped}}ff
|
||||
border={{colors.primary.default.hex_stripped}}ff
|
||||
25
Assets/Matugen/templates/ghostty.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
palette = 0= {{colors.shadow.default.hex}}
|
||||
palette = 1= {{colors.error.default.hex}}
|
||||
palette = 2= {{colors.tertiary.default.hex}}
|
||||
palette = 3= {{colors.secondary.default.hex}}
|
||||
palette = 4= {{colors.primary.default.hex}}
|
||||
palette = 5= {{colors.primary.default.hex}}
|
||||
palette = 6= {{colors.secondary.default.hex}}
|
||||
palette = 7= {{colors.on_background.default.hex}}
|
||||
palette = 8= {{colors.outline.default.hex}}
|
||||
palette = 9= {{colors.secondary_fixed_dim.default.hex}}
|
||||
palette = 10= {{colors.tertiary_container.default.hex}}
|
||||
palette = 11= {{colors.surface_container.default.hex}}
|
||||
palette = 12= {{colors.primary_container.default.hex}}
|
||||
palette = 13= {{colors.on_primary_container.default.hex}}
|
||||
palette = 14= {{colors.surface_variant.default.hex}}
|
||||
palette = 15= {{colors.primary.default.hex}}
|
||||
|
||||
foreground={{colors.on_surface.default.hex}}
|
||||
background={{colors.surface.default.hex}}
|
||||
|
||||
cursor-color = {{colors.on_surface.default.hex}}
|
||||
cursor-text = {{colors.on_surface.default.hex}}
|
||||
|
||||
selection-background = {{colors.on_secondary.default.hex}}
|
||||
selection-foreground = {{colors.secondary_fixed_dim.default.hex}}
|
||||
@@ -1,40 +1,25 @@
|
||||
cursor {{colors.on_surface.default.hex}}
|
||||
cursor_text_color {{colors.on_surface_variant.default.hex}}
|
||||
color0 {{colors.surface.default.hex}}
|
||||
color1 {{colors.error.default.hex}}
|
||||
color2 {{colors.tertiary.default.hex}}
|
||||
color3 {{colors.secondary.default.hex}}
|
||||
color4 {{colors.primary.default.hex}}
|
||||
color5 {{colors.primary.default.hex}}
|
||||
color6 {{colors.secondary.default.hex}}
|
||||
color7 {{colors.on_background.default.hex}}
|
||||
color8 {{colors.outline.default.hex}}
|
||||
color9 {{colors.secondary_fixed_dim.default.hex}}
|
||||
color10 {{colors.tertiary_container.default.hex}}
|
||||
color11 {{colors.surface_container.default.hex}}
|
||||
color12 {{colors.primary_container.default.hex}}
|
||||
color13 {{colors.on_primary_container.default.hex}}
|
||||
color14 {{colors.surface_variant.default.hex}}
|
||||
color15 {{colors.on_background.default.hex}}
|
||||
|
||||
cursor {{colors.primary.default.hex}}
|
||||
cursor_text_color {{colors.on_surface.default.hex}}
|
||||
|
||||
foreground {{colors.on_surface.default.hex}}
|
||||
background {{colors.surface.default.hex}}
|
||||
selection_foreground {{colors.on_secondary.default.hex}}
|
||||
selection_background {{colors.secondary.default.hex}}
|
||||
selection_background {{colors.secondary_fixed_dim.default.hex}}
|
||||
url_color {{colors.primary.default.hex}}
|
||||
|
||||
# black
|
||||
color0 {{colors.surface_container.default.hex}}
|
||||
color8 {{colors.on_surface_variant.default.hex}}
|
||||
|
||||
# red
|
||||
color1 {{colors.error.default.hex}}
|
||||
color9 {{colors.on_error.default.hex}}
|
||||
|
||||
# green
|
||||
color2 {{colors.tertiary.default.hex}}
|
||||
color10 {{colors.on_tertiary.default.hex}}
|
||||
|
||||
# yellow
|
||||
color3 {{colors.secondary.default.hex}}
|
||||
color11 {{colors.on_secondary.default.hex}}
|
||||
|
||||
# blue
|
||||
color4 {{colors.primary.default.hex}}
|
||||
color12 {{colors.on_primary.default.hex}}
|
||||
|
||||
# magenta
|
||||
color5 {{colors.surface_container.default.hex}}
|
||||
color13 {{colors.on_surface.default.hex}}
|
||||
|
||||
# cyan
|
||||
color6 {{colors.outline_variant.default.hex}}
|
||||
color14 {{colors.on_surface_variant.default.hex}}
|
||||
|
||||
# white
|
||||
color7 {{colors.on_surface_variant.default.hex}}
|
||||
color15 {{colors.on_surface.default.hex}}
|
||||
|
||||
22
Assets/Matugen/templates/pywalfox.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"wallpaper": "{{image}}",
|
||||
"alpha": "100",
|
||||
"colors": {
|
||||
"color0": "{{colors.background.default.hex}}",
|
||||
"color1": "",
|
||||
"color2": "",
|
||||
"color3": "",
|
||||
"color4": "",
|
||||
"color5": "",
|
||||
"color6": "",
|
||||
"color7": "",
|
||||
"color8": "",
|
||||
"color9": "",
|
||||
"color10": "{{colors.primary.default.hex}}",
|
||||
"color11": "",
|
||||
"color12": "",
|
||||
"color13": "{{colors.surface_bright.default.hex}}",
|
||||
"color14": "",
|
||||
"color15": "{{colors.on_surface.default.hex}}"
|
||||
}
|
||||
}
|
||||
113
Assets/Matugen/templates/vesktop.css
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @name noctalia
|
||||
* @description Original theme: midnight | A dark, rounded discord theme.
|
||||
* @author refact0r
|
||||
* @version 1.6.2
|
||||
* @invite nz87hXyvcy
|
||||
* @website https://github.com/refact0r/midnight-discord
|
||||
* @source https://github.com/refact0r/midnight-discord/blob/master/midnight.theme.css
|
||||
* @authorId 508863359777505290
|
||||
* @authorLink https://www.refact0r.dev
|
||||
*/
|
||||
|
||||
/* IMPORTANT: make sure to enable dark mode in discord settings for the theme to apply properly!!! */
|
||||
|
||||
@import url('https://refact0r.github.io/midnight-discord/build/midnight.css');
|
||||
|
||||
/* customize things here */
|
||||
:root {
|
||||
/* font, change to 'gg sans' for default discord font*/
|
||||
--font: 'figtree';
|
||||
|
||||
/* top left corner text */
|
||||
--corner-text: 'Midnight';
|
||||
|
||||
/* color of status indicators and window controls */
|
||||
--online-indicator: {{colors.inverse_primary.default.hex}}; /* change to #23a55a for default green */
|
||||
--dnd-indicator: {{colors.error.default.hex}}; /* change to #f13f43 for default red */
|
||||
--idle-indicator: {{colors.tertiary_container.default.hex}}; /* change to #f0b232 for default yellow */
|
||||
--streaming-indicator: {{colors.on_primary.default.hex}}; /* change to #593695 for default purple */
|
||||
|
||||
/* accent colors */
|
||||
--accent-1: {{colors.tertiary.default.hex}}; /* links */
|
||||
--accent-2: {{colors.primary.default.hex}}; /* general unread/mention elements, some icons when active */
|
||||
--accent-3: {{colors.primary.default.hex}}; /* accent buttons */
|
||||
--accent-4: {{colors.surface_bright.default.hex}}; /* accent buttons when hovered */
|
||||
--accent-5: {{colors.primary_fixed_dim.default.hex}}; /* accent buttons when clicked */
|
||||
--mention: {{colors.surface.default.hex}}; /* mentions & mention messages */
|
||||
--mention-hover: {{colors.surface_bright.default.hex}}; /* mentions & mention messages when hovered */
|
||||
|
||||
/* text colors */
|
||||
--text-0: {{colors.surface.default.hex}}; /* text on colored elements */
|
||||
--text-1: {{colors.on_surface.default.hex}}; /* other normally white text */
|
||||
--text-2: {{colors.on_surface.default.hex}}; /* headings and important text */
|
||||
--text-3: {{colors.on_surface_variant.default.hex}}; /* normal text */
|
||||
--text-4: {{colors.on_surface_variant.default.hex}}; /* icon buttons and channels */
|
||||
--text-5: {{colors.outline.default.hex}}; /* muted channels/chats and timestamps */
|
||||
|
||||
/* background and dark colors */
|
||||
--bg-1: {{colors.primary.default.hex}}; /* dark buttons when clicked */
|
||||
--bg-2: {{colors.surface_container_high.default.hex}}; /* dark buttons */
|
||||
--bg-3: {{colors.surface_container_low.default.hex}}; /* spacing, secondary elements */
|
||||
--bg-4: {{colors.surface.default.hex}}; /* main background color */
|
||||
--hover: {{colors.surface_bright.default.hex}}; /* channels and buttons when hovered */
|
||||
--active: {{colors.surface_bright.default.hex}}; /* channels and buttons when clicked or selected */
|
||||
--message-hover: {{colors.surface_bright.default.hex}}; /* messages when hovered */
|
||||
|
||||
/* amount of spacing and padding */
|
||||
--spacing: 12px;
|
||||
|
||||
/* animations */
|
||||
/* ALL ANIMATIONS CAN BE DISABLED WITH REDUCED MOTION IN DISCORD SETTINGS */
|
||||
--list-item-transition: 0.2s ease; /* channels/members/settings hover transition */
|
||||
--unread-bar-transition: 0.2s ease; /* unread bar moving into view transition */
|
||||
--moon-spin-transition: 0.4s ease; /* moon icon spin */
|
||||
--icon-spin-transition: 1s ease; /* round icon button spin (settings, emoji, etc.) */
|
||||
|
||||
/* corner roundness (border-radius) */
|
||||
--roundness-xl: 22px; /* roundness of big panel outer corners */
|
||||
--roundness-l: 20px; /* popout panels */
|
||||
--roundness-m: 16px; /* smaller panels, images, embeds */
|
||||
--roundness-s: 12px; /* members, settings inputs */
|
||||
--roundness-xs: 10px; /* channels, buttons */
|
||||
--roundness-xxs: 8px; /* searchbar, small elements */
|
||||
|
||||
/* direct messages moon icon */
|
||||
/* change to block to show, none to hide */
|
||||
--discord-icon: none; /* discord icon */
|
||||
--moon-icon: block; /* moon icon */
|
||||
--moon-icon-url: url('https://upload.wikimedia.org/wikipedia/commons/c/c4/Font_Awesome_5_solid_moon.svg'); /* custom icon url */
|
||||
--moon-icon-size: auto;
|
||||
|
||||
/* filter uncolorable elements to fit theme */
|
||||
/* (just set to none, they're too much work to configure) */
|
||||
--login-bg-filter: saturate(0.3) hue-rotate(-15deg) brightness(0.4); /* login background artwork */
|
||||
--green-to-accent-3-filter: hue-rotate(56deg) saturate(1.43); /* add friend page explore icon */
|
||||
--blurple-to-accent-3-filter: hue-rotate(304deg) saturate(0.84) brightness(1.2); /* add friend page school icon */
|
||||
}
|
||||
|
||||
/* Selected chat/friend text */
|
||||
.selected_f5eb4b,
|
||||
.selected_f6f816 .link_d8bfb3 {
|
||||
color: var(--text-0) !important;
|
||||
background: var(--accent-3) !important;
|
||||
}
|
||||
|
||||
.selected_f6f816 .link_d8bfb3 * {
|
||||
color: var(--text-0) !important;
|
||||
fill: var(--text-0) !important;
|
||||
}
|
||||
|
||||
/* Make channel name text less visible (darker) */
|
||||
.name__2ea32 {
|
||||
color: var(--text-5) !important;
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
|
||||
/* Make unread channel names brighter */
|
||||
.link__2ea32[aria-label*="unread"] .name__2ea32 {
|
||||
color: var(--text-2) !important;
|
||||
opacity: 1 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
BIN
Assets/Screenshots/noctalia-dark-1.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
Assets/Screenshots/noctalia-dark-2.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
Assets/Screenshots/noctalia-dark-3.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
Assets/Screenshots/noctalia-light-1.png
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
BIN
Assets/Screenshots/noctalia-light-2.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
Assets/Screenshots/noctalia-light-3.png
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 373 KiB |
1433
Assets/Translations/de.json
Normal file
1433
Assets/Translations/en.json
Normal file
1429
Assets/Translations/es.json
Normal file
1429
Assets/Translations/fr.json
Normal file
1429
Assets/Translations/pt.json
Normal file
1429
Assets/Translations/zh-CN.json
Normal file
BIN
Assets/Wallpaper/noctalia.png
Normal file
|
After Width: | Height: | Size: 760 KiB |
194
Assets/settings-default.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"settingsVersion": 12,
|
||||
"bar": {
|
||||
"position": "top",
|
||||
"backgroundOpacity": 1,
|
||||
"monitors": [],
|
||||
"density": "default",
|
||||
"showCapsule": true,
|
||||
"floating": false,
|
||||
"marginVertical": 0.25,
|
||||
"marginHorizontal": 0.25,
|
||||
"widgets": {
|
||||
"left": [
|
||||
{
|
||||
"id": "SystemMonitor"
|
||||
},
|
||||
{
|
||||
"id": "ActiveWindow"
|
||||
},
|
||||
{
|
||||
"id": "MediaMini"
|
||||
}
|
||||
],
|
||||
"center": [
|
||||
{
|
||||
"id": "Workspace"
|
||||
}
|
||||
],
|
||||
"right": [
|
||||
{
|
||||
"id": "ScreenRecorder"
|
||||
},
|
||||
{
|
||||
"id": "Tray"
|
||||
},
|
||||
{
|
||||
"id": "NotificationHistory"
|
||||
},
|
||||
{
|
||||
"id": "WiFi"
|
||||
},
|
||||
{
|
||||
"id": "Bluetooth"
|
||||
},
|
||||
{
|
||||
"id": "Battery"
|
||||
},
|
||||
{
|
||||
"id": "Volume"
|
||||
},
|
||||
{
|
||||
"id": "Brightness"
|
||||
},
|
||||
{
|
||||
"id": "Clock"
|
||||
},
|
||||
{
|
||||
"id": "ControlCenter"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"avatarImage": "",
|
||||
"dimDesktop": true,
|
||||
"showScreenCorners": false,
|
||||
"forceBlackScreenCorners": false,
|
||||
"radiusRatio": 1,
|
||||
"screenRadiusRatio": 1,
|
||||
"animationSpeed": 1,
|
||||
"animationDisabled": false
|
||||
},
|
||||
"location": {
|
||||
"name": "Tokyo",
|
||||
"useFahrenheit": false,
|
||||
"use12hourFormat": false,
|
||||
"showWeekNumberInCalendar": false
|
||||
},
|
||||
"screenRecorder": {
|
||||
"directory": "",
|
||||
"frameRate": 60,
|
||||
"audioCodec": "opus",
|
||||
"videoCodec": "h264",
|
||||
"quality": "very_high",
|
||||
"colorRange": "limited",
|
||||
"showCursor": true,
|
||||
"audioSource": "default_output",
|
||||
"videoSource": "portal"
|
||||
},
|
||||
"wallpaper": {
|
||||
"enabled": true,
|
||||
"directory": "",
|
||||
"enableMultiMonitorDirectories": false,
|
||||
"setWallpaperOnAllMonitors": true,
|
||||
"fillMode": "crop",
|
||||
"fillColor": "#000000",
|
||||
"randomEnabled": false,
|
||||
"randomIntervalSec": 300,
|
||||
"transitionDuration": 1500,
|
||||
"transitionType": "random",
|
||||
"transitionEdgeSmoothness": 0.05,
|
||||
"monitors": []
|
||||
},
|
||||
"appLauncher": {
|
||||
"enableClipboardHistory": false,
|
||||
"position": "center",
|
||||
"backgroundOpacity": 1,
|
||||
"pinnedExecs": [],
|
||||
"useApp2Unit": false,
|
||||
"sortByMostUsed": true,
|
||||
"terminalCommand": "xterm -e"
|
||||
},
|
||||
"dock": {
|
||||
"autoHide": false,
|
||||
"exclusive": false,
|
||||
"backgroundOpacity": 1,
|
||||
"floatingRatio": 1,
|
||||
"onlySameOutput": true,
|
||||
"monitors": [],
|
||||
"pinnedApps": []
|
||||
},
|
||||
"network": {
|
||||
"wifiEnabled": true
|
||||
},
|
||||
"notifications": {
|
||||
"doNotDisturb": false,
|
||||
"monitors": [],
|
||||
"location": "top_right",
|
||||
"alwaysOnTop": false,
|
||||
"lastSeenTs": 0,
|
||||
"respectExpireTimeout": false,
|
||||
"lowUrgencyDuration": 3,
|
||||
"normalUrgencyDuration": 8,
|
||||
"criticalUrgencyDuration": 15
|
||||
},
|
||||
"osd": {
|
||||
"enabled": true,
|
||||
"location": "top_right",
|
||||
"monitors": [],
|
||||
"autoHideMs": 2000
|
||||
},
|
||||
"audio": {
|
||||
"volumeStep": 5,
|
||||
"volumeOverdrive": false,
|
||||
"cavaFrameRate": 60,
|
||||
"visualizerType": "linear",
|
||||
"mprisBlacklist": [],
|
||||
"preferredPlayer": ""
|
||||
},
|
||||
"ui": {
|
||||
"fontDefault": "Roboto",
|
||||
"fontFixed": "DejaVu Sans Mono",
|
||||
"fontDefaultScale": 1,
|
||||
"fontFixedScale": 1,
|
||||
"monitorsScaling": [],
|
||||
"idleInhibitorEnabled": false
|
||||
},
|
||||
"brightness": {
|
||||
"brightnessStep": 5
|
||||
},
|
||||
"colorSchemes": {
|
||||
"useWallpaperColors": false,
|
||||
"predefinedScheme": "Noctalia (default)",
|
||||
"darkMode": true,
|
||||
"matugenSchemeType": "scheme-fruit-salad"
|
||||
},
|
||||
"matugen": {
|
||||
"gtk4": false,
|
||||
"gtk3": false,
|
||||
"qt6": false,
|
||||
"qt5": false,
|
||||
"kitty": false,
|
||||
"ghostty": false,
|
||||
"foot": false,
|
||||
"fuzzel": false,
|
||||
"vesktop": false,
|
||||
"pywalfox": false,
|
||||
"enableUserTemplates": false
|
||||
},
|
||||
"nightLight": {
|
||||
"enabled": false,
|
||||
"forced": false,
|
||||
"autoSchedule": true,
|
||||
"nightTemp": "4000",
|
||||
"dayTemp": "6500",
|
||||
"manualSunrise": "06:30",
|
||||
"manualSunset": "18:30"
|
||||
},
|
||||
"hooks": {
|
||||
"enabled": false,
|
||||
"wallpaperChange": "",
|
||||
"darkModeChange": ""
|
||||
}
|
||||
}
|
||||
636
Bin/i18n-json-check.sh
Executable file
@@ -0,0 +1,636 @@
|
||||
#!/bin/bash
|
||||
|
||||
# JSON Language File Comparison Script
|
||||
# Compares language files against en.json reference and generates a report
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
FOLDER_PATH="Assets/Translations"
|
||||
REFERENCE_FILE="en.json"
|
||||
TRANSLATE_MODE=false
|
||||
|
||||
# Colors for terminal output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_color() {
|
||||
local color=$1
|
||||
local message=$2
|
||||
echo -e "${color}${message}${NC}"
|
||||
}
|
||||
|
||||
# Function to check if jq is installed
|
||||
check_dependencies() {
|
||||
if ! command -v jq &> /dev/null; then
|
||||
print_color $RED "Error: 'jq' is required but not installed. Please install jq first." >&2
|
||||
print_color $YELLOW "On Ubuntu/Debian: sudo apt-get install jq" >&2
|
||||
print_color $YELLOW "On CentOS/RHEL: sudo yum install jq" >&2
|
||||
print_color $YELLOW "On macOS: brew install jq" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if $TRANSLATE_MODE && ! command -v curl &> /dev/null; then
|
||||
print_color $RED "Error: 'curl' is required for translation mode but not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get Gemini API key
|
||||
get_gemini_api_key() {
|
||||
if [[ -z "${GEMINI_API_KEY:-}" ]]; then
|
||||
print_color $RED "Error: GEMINI_API_KEY environment variable is not set" >&2
|
||||
print_color $YELLOW "Please set it with: export GEMINI_API_KEY='your-api-key'" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$GEMINI_API_KEY"
|
||||
}
|
||||
|
||||
# Function to get value from JSON using key path
|
||||
get_json_value() {
|
||||
local json_file=$1
|
||||
local key_path=$2
|
||||
|
||||
# Convert dot-separated path to jq path
|
||||
local jq_path=$(echo "$key_path" | sed 's/\./\.\["/g' | sed 's/$/"]/' | sed 's/^\.//')
|
||||
local jq_query=".${jq_path}"
|
||||
|
||||
# Use a more robust approach: split by dots and build path
|
||||
local -a path_parts
|
||||
IFS='.' read -ra path_parts <<< "$key_path"
|
||||
|
||||
local jq_filter="."
|
||||
for part in "${path_parts[@]}"; do
|
||||
jq_filter="${jq_filter}[\"${part}\"]"
|
||||
done
|
||||
|
||||
jq -r "$jq_filter // empty" "$json_file" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
# Function to list available Gemini models
|
||||
list_gemini_models() {
|
||||
local api_key=$(get_gemini_api_key)
|
||||
|
||||
print_color $BLUE "Fetching available Gemini models..." >&2
|
||||
echo "" >&2
|
||||
|
||||
local response=$(curl -s -X GET \
|
||||
"https://generativelanguage.googleapis.com/v1/models?key=${api_key}" \
|
||||
-H "Content-Type: application/json" 2>/dev/null)
|
||||
|
||||
# Parse and display models
|
||||
echo "$response" | jq -r '.models[] | "- \(.name) (\(.displayName))"' 2>/dev/null || {
|
||||
print_color $RED "Failed to parse models list" >&2
|
||||
echo "$response" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Function to translate text using Gemini API
|
||||
translate_text() {
|
||||
local text=$1
|
||||
local target_language=$2
|
||||
local api_key=$3
|
||||
|
||||
# Escape text for JSON
|
||||
local escaped_text=$(echo "$text" | jq -Rs .)
|
||||
|
||||
# Prepare the API request
|
||||
local prompt="Translate the following English text to ${target_language}. Return ONLY the translation, no explanations or additional text:\n\n${text}"
|
||||
local escaped_prompt=$(echo "$prompt" | jq -Rs .)
|
||||
|
||||
local request_body=$(cat <<EOF
|
||||
{
|
||||
"contents": [{
|
||||
"parts": [{
|
||||
"text": ${escaped_prompt}
|
||||
}]
|
||||
}],
|
||||
"generationConfig": {
|
||||
"temperature": 0.3,
|
||||
"maxOutputTokens": 1000
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Make API call to Gemini
|
||||
local api_url="https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${api_key}"
|
||||
|
||||
print_color $BLUE " API URL: $api_url" >&2
|
||||
|
||||
local response=$(curl -s -X POST "$api_url" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$request_body" 2>/dev/null)
|
||||
|
||||
print_color $BLUE " API Response: $response" >&2
|
||||
|
||||
# Extract the translation from response - try multiple parsing approaches
|
||||
local translation=$(echo "$response" | jq -r '.candidates[0].content.parts[0].text // .text // empty' 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
if [[ -z "$translation" ]]; then
|
||||
print_color $RED " Failed to parse translation. Full response:" >&2
|
||||
echo "$response" | jq . >&2 2>/dev/null || echo "$response" >&2
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_color $GREEN " Parsed translation: $translation" >&2
|
||||
|
||||
echo "$translation"
|
||||
}
|
||||
|
||||
# Function to inject translation into JSON file using jq
|
||||
inject_translation() {
|
||||
local json_file=$1
|
||||
local key_path=$2
|
||||
local value=$3
|
||||
|
||||
# Split key path into array
|
||||
local -a path_parts
|
||||
IFS='.' read -ra path_parts <<< "$key_path"
|
||||
|
||||
# Build jq path array
|
||||
local jq_path="["
|
||||
for i in "${!path_parts[@]}"; do
|
||||
if [[ $i -gt 0 ]]; then
|
||||
jq_path+=","
|
||||
fi
|
||||
jq_path+="\"${path_parts[$i]}\""
|
||||
done
|
||||
jq_path+="]"
|
||||
|
||||
# Create a temporary file
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
# Use jq to set the value at the path
|
||||
jq --argjson path "$jq_path" --arg value "$value" 'setpath($path; $value)' "$json_file" > "$temp_file"
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
mv "$temp_file" "$json_file"
|
||||
return 0
|
||||
else
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to extract all keys from a JSON file recursively
|
||||
extract_keys() {
|
||||
local json_file=$1
|
||||
|
||||
if [[ ! -f "$json_file" ]]; then
|
||||
echo "Error: File $json_file not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract all keys recursively using jq
|
||||
jq -r '
|
||||
def keys_recursive:
|
||||
if type == "object" then
|
||||
keys[] as $k |
|
||||
if (.[$k] | type) == "object" then
|
||||
($k + "." + (.[$k] | keys_recursive))
|
||||
else
|
||||
$k
|
||||
end
|
||||
else
|
||||
empty
|
||||
end;
|
||||
keys_recursive
|
||||
' "$json_file" 2>/dev/null | sort
|
||||
}
|
||||
|
||||
# Function to get language files
|
||||
get_language_files() {
|
||||
find "$FOLDER_PATH" -maxdepth 1 -name "*.json" -type f | sort
|
||||
}
|
||||
|
||||
# Function to generate report header
|
||||
generate_header() {
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo "================================================================================"
|
||||
echo " LANGUAGE FILE COMPARISON REPORT"
|
||||
echo "================================================================================"
|
||||
echo "Generated: $timestamp"
|
||||
echo "Reference file: $REFERENCE_FILE"
|
||||
echo "Folder: $(realpath "$FOLDER_PATH")"
|
||||
if $TRANSLATE_MODE; then
|
||||
echo "Mode: TRANSLATION ENABLED"
|
||||
fi
|
||||
echo ""
|
||||
echo "Notes:"
|
||||
echo "- Keys are compared recursively through all nested JSON objects"
|
||||
echo "- Missing keys indicate incomplete translations"
|
||||
echo "- Extra keys might indicate deprecated keys or translation-specific additions"
|
||||
echo "- Translation completion percentage is calculated based on English reference"
|
||||
echo "- Results are sorted by descending line numbers for easier editing"
|
||||
echo ""
|
||||
echo "This report compares all language JSON files against the English reference file"
|
||||
echo "and identifies missing keys and extra keys in each language."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Function to find line number of a key in JSON file
|
||||
find_key_line_number() {
|
||||
local json_file=$1
|
||||
local key_path=$2
|
||||
|
||||
# Extract the final key name (after last dot)
|
||||
local key_name="${key_path##*.}"
|
||||
|
||||
# Search for the key in the file with line numbers
|
||||
# Look for the pattern "key": (with quotes and colon)
|
||||
local line_num=$(grep -n "\"$key_name\":" "$json_file" 2>/dev/null | head -1 | cut -d: -f1 || echo "")
|
||||
|
||||
if [[ -n "$line_num" ]]; then
|
||||
echo "$line_num"
|
||||
else
|
||||
# If not found with quotes, try without (though less reliable)
|
||||
line_num=$(grep -n "$key_name:" "$json_file" 2>/dev/null | head -1 | cut -d: -f1 || echo "")
|
||||
if [[ -n "$line_num" ]]; then
|
||||
echo "$line_num"
|
||||
else
|
||||
echo "?"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to safely count lines
|
||||
count_non_empty_lines() {
|
||||
local content="$1"
|
||||
if [[ -z "$content" ]]; then
|
||||
echo "0"
|
||||
else
|
||||
echo "$content" | grep -c -v '^$' || echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to compare keys and generate report section
|
||||
compare_language() {
|
||||
local lang_file="$1"
|
||||
local lang_name="$2"
|
||||
local ref_keys_file="$3"
|
||||
local ref_file_path="$FOLDER_PATH/$REFERENCE_FILE"
|
||||
|
||||
# Create temporary file for language keys
|
||||
local lang_keys_file=$(mktemp)
|
||||
extract_keys "$lang_file" > "$lang_keys_file" || {
|
||||
echo "Error: Failed to extract keys from $lang_file" >&2
|
||||
rm -f "$lang_keys_file"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get missing and extra keys safely
|
||||
local missing_keys=""
|
||||
local extra_keys=""
|
||||
|
||||
missing_keys=$(comm -23 "$ref_keys_file" "$lang_keys_file" 2>/dev/null || echo "")
|
||||
extra_keys=$(comm -13 "$ref_keys_file" "$lang_keys_file" 2>/dev/null || echo "")
|
||||
|
||||
# Count lines safely
|
||||
local missing_count=$(count_non_empty_lines "$missing_keys")
|
||||
local extra_count=$(count_non_empty_lines "$extra_keys")
|
||||
local total_ref_keys=$(wc -l < "$ref_keys_file" 2>/dev/null || echo "0")
|
||||
local total_lang_keys=$(wc -l < "$lang_keys_file" 2>/dev/null || echo "0")
|
||||
|
||||
# Calculate completion percentage safely
|
||||
local completion_percentage=0
|
||||
if [[ $total_ref_keys -gt 0 ]]; then
|
||||
completion_percentage=$(( (total_ref_keys - missing_count) * 100 / total_ref_keys ))
|
||||
fi
|
||||
|
||||
print_color $YELLOW "================================================================================"
|
||||
print_color $YELLOW "LANGUAGE: $lang_name"
|
||||
print_color $YELLOW "================================================================================"
|
||||
echo "File: $lang_file"
|
||||
echo "Total keys in reference (en): $total_ref_keys"
|
||||
echo "Total keys in $lang_name: $total_lang_keys"
|
||||
|
||||
# Color code the completion percentage
|
||||
if [[ $completion_percentage -eq 100 ]]; then
|
||||
echo -e "Translation completion: ${GREEN}${completion_percentage}%${NC}"
|
||||
else
|
||||
echo -e "Translation completion: ${RED}${completion_percentage}%${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "SUMMARY:"
|
||||
echo "- Missing keys (exist in English but not in $lang_name): $missing_count"
|
||||
echo "- Extra keys (exist in $lang_name but not in English): $extra_count"
|
||||
echo ""
|
||||
|
||||
# Handle missing keys
|
||||
if [[ $missing_count -gt 0 && -n "$missing_keys" ]]; then
|
||||
echo "MISSING KEYS IN $lang_name:"
|
||||
|
||||
# Collect keys with line numbers and sort by line number (descending)
|
||||
local temp_missing=$(mktemp)
|
||||
while IFS= read -r key; do
|
||||
if [[ -n "$key" ]]; then
|
||||
local ref_line=$(find_key_line_number "$ref_file_path" "$key")
|
||||
# Use numeric sort padding for proper sorting
|
||||
if [[ "$ref_line" =~ ^[0-9]+$ ]]; then
|
||||
printf "%06d|%s|en.json:%s\n" "$ref_line" "$key" "$ref_line" >> "$temp_missing"
|
||||
else
|
||||
printf "999999|%s|en.json:%s\n" "$key" "$ref_line" >> "$temp_missing"
|
||||
fi
|
||||
fi
|
||||
done <<< "$missing_keys"
|
||||
|
||||
# Sort by line number (descending) and display
|
||||
local counter=1
|
||||
sort -t'|' -k1,1nr "$temp_missing" | while IFS='|' read -r sort_key key location; do
|
||||
printf " %3d. %s (%s)\n" "$counter" "$key" "$location"
|
||||
counter=$((counter + 1))
|
||||
done
|
||||
rm -f "$temp_missing"
|
||||
echo ""
|
||||
|
||||
# Translate missing keys if in translate mode
|
||||
if $TRANSLATE_MODE; then
|
||||
print_color $BLUE "Translating missing keys for $lang_name..." >&2
|
||||
local api_key=$(get_gemini_api_key)
|
||||
local translated_count=0
|
||||
local failed_count=0
|
||||
|
||||
while IFS= read -r key; do
|
||||
if [[ -n "$key" ]]; then
|
||||
# Get English value
|
||||
local en_value=$(get_json_value "$ref_file_path" "$key")
|
||||
|
||||
if [[ -n "$en_value" ]]; then
|
||||
print_color $YELLOW " Translating: $key" >&2
|
||||
|
||||
# Translate the value
|
||||
local translated_value=$(translate_text "$en_value" "$lang_name" "$api_key")
|
||||
|
||||
if [[ -n "$translated_value" ]]; then
|
||||
# Inject translation into the file
|
||||
if inject_translation "$lang_file" "$key" "$translated_value"; then
|
||||
print_color $GREEN " ✓ Translated: $key" >&2
|
||||
translated_count=$((translated_count + 1))
|
||||
else
|
||||
print_color $RED " ✗ Failed to inject: $key" >&2
|
||||
failed_count=$((failed_count + 1))
|
||||
fi
|
||||
else
|
||||
print_color $RED " ✗ Translation failed: $key" >&2
|
||||
failed_count=$((failed_count + 1))
|
||||
fi
|
||||
|
||||
# Small delay to avoid rate limiting
|
||||
sleep 0.5
|
||||
fi
|
||||
fi
|
||||
done <<< "$missing_keys"
|
||||
|
||||
echo ""
|
||||
print_color $GREEN "Translation complete: $translated_count succeeded, $failed_count failed" >&2
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
echo "✅ No missing keys in $lang_name"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Handle extra keys
|
||||
if [[ $extra_count -gt 0 && -n "$extra_keys" ]]; then
|
||||
echo "EXTRA KEYS IN $lang_name (not in English):"
|
||||
|
||||
# Collect keys with line numbers and sort by line number (descending)
|
||||
local temp_extra=$(mktemp)
|
||||
while IFS= read -r key; do
|
||||
if [[ -n "$key" ]]; then
|
||||
local lang_line=$(find_key_line_number "$lang_file" "$key")
|
||||
# Use numeric sort padding for proper sorting
|
||||
if [[ "$lang_line" =~ ^[0-9]+$ ]]; then
|
||||
printf "%06d|%s|%s:%s\n" "$lang_line" "$key" "$(basename "$lang_file")" "$lang_line" >> "$temp_extra"
|
||||
else
|
||||
printf "999999|%s|%s:%s\n" "$key" "$(basename "$lang_file")" "$lang_line" >> "$temp_extra"
|
||||
fi
|
||||
fi
|
||||
done <<< "$extra_keys"
|
||||
|
||||
# Sort by line number (descending) and display
|
||||
local counter=1
|
||||
sort -t'|' -k1,1nr "$temp_extra" | while IFS='|' read -r sort_key key location; do
|
||||
printf " %3d. %s (%s)\n" "$counter" "$key" "$location"
|
||||
counter=$((counter + 1))
|
||||
done
|
||||
rm -f "$temp_extra"
|
||||
echo ""
|
||||
else
|
||||
echo "✅ No extra keys in $lang_name"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f "$lang_keys_file"
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
local target_language="$1"
|
||||
|
||||
print_color $BLUE "Starting language file comparison..." >&2
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies
|
||||
|
||||
# Validate folder path
|
||||
if [[ ! -d "$FOLDER_PATH" ]]; then
|
||||
print_color $RED "Error: Folder '$FOLDER_PATH' does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if reference file exists
|
||||
local ref_file_path="$FOLDER_PATH/$REFERENCE_FILE"
|
||||
if [[ ! -f "$ref_file_path" ]]; then
|
||||
print_color $RED "Error: Reference file '$ref_file_path' does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_color $GREEN "Reference file found: $ref_file_path" >&2
|
||||
|
||||
# Extract keys from reference file
|
||||
local ref_keys_file=$(mktemp)
|
||||
if ! extract_keys "$ref_file_path" > "$ref_keys_file"; then
|
||||
print_color $RED "Error: Failed to extract keys from reference file" >&2
|
||||
rm -f "$ref_keys_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local total_ref_keys=$(wc -l < "$ref_keys_file" 2>/dev/null || echo "0")
|
||||
|
||||
print_color $BLUE "Extracted $total_ref_keys keys from reference file" >&2
|
||||
|
||||
# Get all language files or just the target language
|
||||
local -a language_files
|
||||
if [[ -n "$target_language" ]]; then
|
||||
# Single language mode
|
||||
local target_file="$FOLDER_PATH/${target_language}.json"
|
||||
if [[ ! -f "$target_file" ]]; then
|
||||
print_color $RED "Error: Language file '$target_file' does not exist" >&2
|
||||
rm -f "$ref_keys_file"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$target_language" == "${REFERENCE_FILE%.json}" ]]; then
|
||||
print_color $RED "Error: Cannot compare reference file against itself" >&2
|
||||
rm -f "$ref_keys_file"
|
||||
exit 1
|
||||
fi
|
||||
language_files=("$target_file")
|
||||
print_color $BLUE "Checking single language: $target_language" >&2
|
||||
else
|
||||
# All languages mode
|
||||
while IFS= read -r -d '' file; do
|
||||
language_files+=("$file")
|
||||
done < <(find "$FOLDER_PATH" -maxdepth 1 -name "*.json" -type f -print0 | sort -z)
|
||||
|
||||
if [[ ${#language_files[@]} -eq 0 ]]; then
|
||||
print_color $RED "Error: No JSON files found in $FOLDER_PATH" >&2
|
||||
rm -f "$ref_keys_file"
|
||||
exit 1
|
||||
fi
|
||||
print_color $BLUE "Found ${#language_files[@]} JSON files to process" >&2
|
||||
fi
|
||||
|
||||
echo "" >&2
|
||||
|
||||
# Generate report header
|
||||
generate_header
|
||||
|
||||
local processed=0
|
||||
for lang_file in "${language_files[@]}"; do
|
||||
local filename=$(basename "$lang_file")
|
||||
local lang_name="${filename%.json}"
|
||||
|
||||
# Skip the reference file in all-languages mode
|
||||
if [[ -z "$target_language" && "$filename" == "$REFERENCE_FILE" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
print_color $YELLOW "Processing: $filename" >&2
|
||||
|
||||
# Validate JSON syntax
|
||||
if ! jq empty "$lang_file" 2>/dev/null; then
|
||||
print_color $RED "Warning: $lang_file contains invalid JSON syntax. Skipping..." >&2
|
||||
echo "ERROR: $lang_file contains invalid JSON syntax and was skipped."
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
if compare_language "$lang_file" "$lang_name" "$ref_keys_file"; then
|
||||
processed=$((processed + 1))
|
||||
else
|
||||
print_color $RED "Error processing $lang_file" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
# Add summary at the end
|
||||
echo "================================================================================"
|
||||
echo "SUMMARY"
|
||||
echo "================================================================================"
|
||||
echo "Total files processed: $processed"
|
||||
echo "Reference file: $REFERENCE_FILE (English)"
|
||||
if [[ -n "$target_language" ]]; then
|
||||
echo "Target language: $target_language"
|
||||
fi
|
||||
if $TRANSLATE_MODE; then
|
||||
echo "Translation mode: ENABLED"
|
||||
fi
|
||||
echo "Report generated: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo ""
|
||||
echo "================================================================================"
|
||||
|
||||
# Clean up
|
||||
rm -f "$ref_keys_file"
|
||||
|
||||
if [[ -n "$target_language" ]]; then
|
||||
print_color $GREEN "Comparison completed for language: $target_language" >&2
|
||||
else
|
||||
print_color $GREEN "Comparison completed: Processed $processed language files against English reference" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage information
|
||||
show_usage() {
|
||||
echo "Usage: $0 [--translate] [language_code]" >&2
|
||||
echo "" >&2
|
||||
echo "This script compares JSON language files in '$FOLDER_PATH' against the English reference." >&2
|
||||
echo "" >&2
|
||||
echo "Arguments:" >&2
|
||||
echo " --translate Enable automatic translation of missing keys using Gemini API" >&2
|
||||
echo " --list-models List all available Gemini models and exit" >&2
|
||||
echo " language_code Optional. Compare only the specified language (e.g., 'fr', 'es', 'de')" >&2
|
||||
echo " If not provided, all language files will be compared" >&2
|
||||
echo "" >&2
|
||||
echo "Configuration:" >&2
|
||||
echo " - Folder path: $FOLDER_PATH (hardcoded)" >&2
|
||||
echo " - Reference file: $REFERENCE_FILE" >&2
|
||||
echo "" >&2
|
||||
echo "Examples:" >&2
|
||||
echo " $0 # Compare all languages" >&2
|
||||
echo " $0 fr # Compare only French (fr.json)" >&2
|
||||
echo " $0 --list-models # List available Gemini models" >&2
|
||||
echo " $0 --translate # Compare all and translate missing keys" >&2
|
||||
echo " $0 --translate fr # Translate missing keys for French only" >&2
|
||||
echo "" >&2
|
||||
echo "Requirements:" >&2
|
||||
echo " - jq must be installed" >&2
|
||||
echo " - curl must be installed (for --translate mode)" >&2
|
||||
echo " - $REFERENCE_FILE must exist in $FOLDER_PATH" >&2
|
||||
echo " - Target language file must exist if specified" >&2
|
||||
echo " - GEMINI_API_KEY environment variable must be set (for --translate mode)" >&2
|
||||
echo "" >&2
|
||||
echo "Output:" >&2
|
||||
echo " - Comparison report is printed to stdout" >&2
|
||||
echo " - Progress messages are printed to stderr" >&2
|
||||
echo " - Results are sorted by descending line numbers for easier editing" >&2
|
||||
}
|
||||
|
||||
# Handle command line arguments
|
||||
target_lang=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
--list-models)
|
||||
list_gemini_models
|
||||
;;
|
||||
--translate)
|
||||
TRANSLATE_MODE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
if [[ -n "$target_lang" ]]; then
|
||||
echo "Error: Too many arguments. Only one language code is allowed." >&2
|
||||
echo "" >&2
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
# Validate language code format (basic check for reasonable filename)
|
||||
if [[ ! "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then
|
||||
echo "Error: Invalid language code format '$1'. Use alphanumeric characters, hyphens, and underscores only." >&2
|
||||
echo "" >&2
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
target_lang="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Run main function
|
||||
main "$target_lang"
|
||||
31
Bin/i18n-qml-check.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Comprehensive i18n checker for QML files
|
||||
# Finds hardcoded strings in various QML properties
|
||||
|
||||
find . -name "*.qml" -type f | while read -r file; do
|
||||
# Skip if file doesn't exist or is not readable
|
||||
[[ ! -r "$file" ]] && continue
|
||||
|
||||
# Check for hardcoded strings in common properties
|
||||
# Matches: property: "text with letters" but excludes I18n.tr calls
|
||||
issues=$(grep -n -E '(label|text|title|description|placeholder|tooltipText|tooltip):\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
|
||||
|
||||
# Also check for template literals with hardcoded text
|
||||
template_issues=$(grep -n -E '(label|text|title|description|placeholder|tooltipText|tooltip):\s*`[^`]*[a-zA-Z][^`]*`' "$file" | grep -v 'I18n\.tr')
|
||||
|
||||
# Check for property assignments with hardcoded strings
|
||||
property_issues=$(grep -n -E 'property\s+string\s+\w+:\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
|
||||
|
||||
# Check for JavaScript object properties with hardcoded strings (like in arrays/models)
|
||||
js_object_issues=$(grep -n -E '"(label|text|title|description|placeholder|name)":\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
|
||||
|
||||
if [[ -n "$issues" || -n "$template_issues" || -n "$property_issues" || -n "$js_object_issues" ]]; then
|
||||
echo "$file"
|
||||
[[ -n "$issues" ]] && echo "$issues"
|
||||
[[ -n "$template_issues" ]] && echo "$template_issues"
|
||||
[[ -n "$property_issues" ]] && echo "$property_issues"
|
||||
[[ -n "$js_object_issues" ]] && echo "$js_object_issues"
|
||||
echo
|
||||
fi
|
||||
done
|
||||
46
Bin/notifications-test.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env -S bash
|
||||
|
||||
echo "Sending test notifications..."
|
||||
|
||||
# Send a bunch of notifications with numbers
|
||||
for i in {1..4}; do
|
||||
notify-send "Notification $i" "This is test notification number $i with a very long text that will probably break the layout or maybe not? Who knows? Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "All notifications sent!"
|
||||
|
||||
# Additional tests for icon/image handling
|
||||
if command -v notify-send >/dev/null 2>&1; then
|
||||
echo "Sending icon/image tests..."
|
||||
|
||||
# 1) Themed icon name
|
||||
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
|
||||
|
||||
# 2) Absolute path if a sample image exists
|
||||
SAMPLE_IMG="/usr/share/pixmaps/steam.png"
|
||||
if [ -f "$SAMPLE_IMG" ]; then
|
||||
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
|
||||
fi
|
||||
|
||||
# 3) file:// URL form
|
||||
if [ -f "$SAMPLE_IMG" ]; then
|
||||
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
|
||||
fi
|
||||
|
||||
echo "Icon/image tests sent!"
|
||||
fi
|
||||
|
||||
# A test notification with actions
|
||||
gdbus call --session \
|
||||
--dest org.freedesktop.Notifications \
|
||||
--object-path /org/freedesktop/Notifications \
|
||||
--method org.freedesktop.Notifications.Notify \
|
||||
"my-app" \
|
||||
0 \
|
||||
"dialog-question" \
|
||||
"Confirmation Required" \
|
||||
"Do you want to proceed with the action?" \
|
||||
"['default', 'OK', 'cancel', 'Cancel', 'maybe', 'Maybe', 'undecided', 'Undecided']" \
|
||||
"{}" \
|
||||
5000
|
||||
@@ -4,4 +4,4 @@
|
||||
# Can be installed from AUR "qmlfmt-git"
|
||||
# Requires qt6-5compat
|
||||
|
||||
find . -name "*.qml" -print -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \;
|
||||
find . -name "*.qml" -print -exec qmlfmt -e -b 360 -t 2 -i 2 -w {} \;
|
||||
|
||||
36
Bin/shaders-compile.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Directory containing the source shaders.
|
||||
SOURCE_DIR="Shaders/frag/"
|
||||
|
||||
# Directory where the compiled shaders will be saved.
|
||||
DEST_DIR="Shaders/qsb/"
|
||||
|
||||
# Check if the source directory exists.
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "Source directory $SOURCE_DIR not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create the destination directory if it doesn't exist.
|
||||
mkdir -p "$DEST_DIR"
|
||||
|
||||
# Loop through all files in the source directory ending with .frag
|
||||
for shader in "$SOURCE_DIR"*.frag; do
|
||||
# Check if a file was found (to handle the case of no .frag files).
|
||||
if [ -f "$shader" ]; then
|
||||
# Get the base name of the file (e.g., wp_fade).
|
||||
shader_name=$(basename "$shader" .frag)
|
||||
|
||||
# Construct the output path for the compiled shader.
|
||||
output_path="$DEST_DIR$shader_name.frag.qsb"
|
||||
|
||||
# Construct and run the qsb command.
|
||||
qsb --qt6 -o "$output_path" "$shader"
|
||||
|
||||
# Print a message to confirm compilation.
|
||||
echo "Compiled $shader to $output_path"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Shader compilation complete."
|
||||
@@ -1,219 +0,0 @@
|
||||
#!/usr/bin/env -S bash
|
||||
|
||||
# A Bash script to monitor system stats and output them in JSON format.
|
||||
|
||||
# --- Configuration ---
|
||||
# Default sleep duration in seconds. Can be overridden by the first argument.
|
||||
SLEEP_DURATION=3
|
||||
|
||||
# --- Argument Parsing ---
|
||||
# Check if a command-line argument is provided for the sleep duration.
|
||||
if [[ -n "$1" ]]; then
|
||||
# Basic validation to ensure the argument is a number (integer or float).
|
||||
if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
||||
SLEEP_DURATION=$1
|
||||
else
|
||||
# Output to stderr if the format is invalid.
|
||||
echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Global Cache Variables ---
|
||||
# These variables will store the discovered CPU temperature sensor path and type
|
||||
# to avoid searching for it on every loop iteration.
|
||||
TEMP_SENSOR_PATH=""
|
||||
TEMP_SENSOR_TYPE=""
|
||||
|
||||
# --- Data Collection Functions ---
|
||||
|
||||
#
|
||||
# Gets memory usage in GB, MB, and as a percentage.
|
||||
#
|
||||
get_memory_info() {
|
||||
awk '
|
||||
/MemTotal/ {total=$2}
|
||||
/MemAvailable/ {available=$2}
|
||||
END {
|
||||
if (total > 0) {
|
||||
usage_kb = total - available
|
||||
usage_gb = usage_kb / 1000000
|
||||
usage_percent = (usage_kb / total) * 100
|
||||
printf "%.1f %.0f\n", usage_gb, usage_percent
|
||||
} else {
|
||||
# Fallback if /proc/meminfo is unreadable or empty.
|
||||
print "0.0 0 0"
|
||||
}
|
||||
}
|
||||
' /proc/meminfo
|
||||
}
|
||||
|
||||
#
|
||||
# Gets the usage percentage of the root filesystem ("/").
|
||||
#
|
||||
get_disk_usage() {
|
||||
# df gets disk usage. --output=pcent shows only the percentage for the root path.
|
||||
# tail -1 gets the data line, and tr removes the '%' sign and whitespace.
|
||||
df --output=pcent / | tail -1 | tr -d ' %'
|
||||
}
|
||||
|
||||
#
|
||||
# Calculates current CPU usage over a short interval.
|
||||
#
|
||||
get_cpu_usage() {
|
||||
# Read all 10 CPU time fields to prevent errors on newer kernels.
|
||||
read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat
|
||||
|
||||
# Calculate previous total and idle times.
|
||||
local prev_total_idle=$((prev_idle + prev_iowait))
|
||||
local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice))
|
||||
|
||||
# Wait for a short period.
|
||||
sleep 0.05
|
||||
|
||||
# Read all 10 CPU time fields again for the second measurement.
|
||||
read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
|
||||
|
||||
# Calculate new total and idle times.
|
||||
local total_idle=$((idle + iowait))
|
||||
local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice))
|
||||
|
||||
# Add a check to prevent division by zero if total hasn't changed.
|
||||
if (( total <= prev_total )); then
|
||||
echo "0.0"
|
||||
return
|
||||
fi
|
||||
|
||||
# Calculate the difference over the interval.
|
||||
local diff_total=$((total - prev_total))
|
||||
local diff_idle=$((total_idle - prev_total_idle))
|
||||
|
||||
# Use awk for floating-point calculation and print the percentage.
|
||||
awk -v total="$diff_total" -v idle="$diff_idle" '
|
||||
BEGIN {
|
||||
if (total > 0) {
|
||||
# Formula: 100 * (Total - Idle) / Total
|
||||
usage = 100 * (total - idle) / total
|
||||
printf "%.1f\n", usage
|
||||
} else {
|
||||
print "0.0"
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
#
|
||||
# Finds and returns the CPU temperature in degrees Celsius.
|
||||
# Caches the sensor path for efficiency.
|
||||
#
|
||||
get_cpu_temp() {
|
||||
# If the sensor path hasn't been found yet, search for it.
|
||||
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
|
||||
for dir in /sys/class/hwmon/hwmon*; do
|
||||
# Check if the 'name' file exists and read it.
|
||||
if [[ -f "$dir/name" ]]; then
|
||||
local name
|
||||
name=$(<"$dir/name")
|
||||
# Check for supported sensor types.
|
||||
if [[ "$name" == "coretemp" || "$name" == "k10temp" || "$name" == "zenpower" ]]; then
|
||||
TEMP_SENSOR_PATH=$dir
|
||||
TEMP_SENSOR_TYPE=$name
|
||||
break # Found it, no need to keep searching.
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# If after searching no sensor was found, return 0.
|
||||
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
|
||||
echo 0
|
||||
return
|
||||
fi
|
||||
|
||||
# --- Get temp based on sensor type ---
|
||||
if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then
|
||||
# For Intel 'coretemp', average all available temperature sensors.
|
||||
local total_temp=0
|
||||
local sensor_count=0
|
||||
|
||||
# Use a for loop with a glob to iterate over all temp input files.
|
||||
# This is more efficient than 'find' for this simple case.
|
||||
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
|
||||
# The glob returns the pattern itself if no files match,
|
||||
# so we must check if the file actually exists.
|
||||
if [[ -f "$temp_file" ]]; then
|
||||
total_temp=$((total_temp + $(<"$temp_file")))
|
||||
sensor_count=$((sensor_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if (( sensor_count > 0 )); then
|
||||
# Use awk for the final division to handle potential floating point numbers
|
||||
# and convert from millidegrees to integer degrees Celsius.
|
||||
awk -v total="$total_temp" -v count="$sensor_count" 'BEGIN { print int(total / count / 1000) }'
|
||||
else
|
||||
# If no sensor files were found, return 0.
|
||||
echo 0
|
||||
fi
|
||||
|
||||
elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then
|
||||
# For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature.
|
||||
local tctl_input=""
|
||||
for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do
|
||||
if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then
|
||||
# The input file has the same name but with '_input' instead of '_label'.
|
||||
tctl_input="${label_file%_label}_input"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -f "$tctl_input" ]]; then
|
||||
# Read the temperature and convert from millidegrees to degrees.
|
||||
echo "$(( $(<"$tctl_input") / 1000 ))"
|
||||
else
|
||||
echo 0 # Fallback
|
||||
fi
|
||||
elif [[ "$TEMP_SENSOR_TYPE" == "zenpower" ]]; then
|
||||
# For zenpower, read the first available temp sensor
|
||||
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
|
||||
if [[ -f "$temp_file" ]]; then
|
||||
local temp_value
|
||||
temp_value=$(cat "$temp_file" | tr -d '\n\r') # Remove any newlines
|
||||
echo "$((temp_value / 1000))"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo 0
|
||||
|
||||
if [[ -f "$tctl_input" ]]; then
|
||||
# Read the temperature and convert from millidegrees to degrees.
|
||||
echo "$(($(<"$tctl_input") / 1000))"
|
||||
else
|
||||
echo 0 # Fallback
|
||||
fi
|
||||
else
|
||||
echo 0 # Should not happen if cache logic is correct.
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main Loop ---
|
||||
# This loop runs indefinitely, gathering and printing stats.
|
||||
while true; do
|
||||
# Call the functions to gather all the data.
|
||||
# get_memory_info
|
||||
read -r mem_gb mem_per <<< "$(get_memory_info)"
|
||||
|
||||
# Command substitution captures the single output from the other functions.
|
||||
disk_per=$(get_disk_usage)
|
||||
cpu_usage=$(get_cpu_usage)
|
||||
cpu_temp=$(get_cpu_temp)
|
||||
|
||||
# Use printf to format the final JSON output string, adding the mem_mb key.
|
||||
printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s"}\n' \
|
||||
"$cpu_usage" \
|
||||
"$cpu_temp" \
|
||||
"$mem_gb" \
|
||||
"$mem_per" \
|
||||
"$disk_per"
|
||||
|
||||
# Wait for the specified duration before the next update.
|
||||
sleep "$SLEEP_DURATION"
|
||||
done
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env -S bash
|
||||
|
||||
echo "Sending 8 test notifications..."
|
||||
|
||||
# Send 8 notifications with numbers
|
||||
for i in {1..8}; do
|
||||
notify-send "Notification $i" "This is test notification number $i of 8"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "All notifications sent!"
|
||||
@@ -44,14 +44,6 @@ Singleton {
|
||||
|
||||
property color transparent: "transparent"
|
||||
|
||||
// -----------
|
||||
function applyOpacity(color, opacity) {
|
||||
// Convert color to string and apply opacity
|
||||
if (!color)
|
||||
return "transparent"
|
||||
return color.toString().replace("#", "#" + opacity)
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Default colors: RosePine
|
||||
QtObject {
|
||||
@@ -110,7 +102,8 @@ Singleton {
|
||||
// FileView to load custom colors data from colors.json
|
||||
FileView {
|
||||
id: customColorsFile
|
||||
path: Settings.configDir + "colors.json"
|
||||
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onFileChanged: {
|
||||
Logger.log("Color", "Reloading colors from disk")
|
||||
@@ -120,6 +113,13 @@ Singleton {
|
||||
Logger.log("Color", "Writing colors to disk")
|
||||
writeAdapter()
|
||||
}
|
||||
|
||||
// Trigger initial load when path changes from empty to actual path
|
||||
onPathChanged: {
|
||||
if (path !== undefined) {
|
||||
reload()
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
if (error.toString().includes("No such file") || error === 2) {
|
||||
// File doesn't exist, create it with default values
|
||||
|
||||
365
Commons/I18n.qml
Normal file
@@ -0,0 +1,365 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool debug: false
|
||||
property string debugForceLanguage: ""
|
||||
|
||||
property bool isLoaded: false
|
||||
property string langCode: ""
|
||||
property var availableLanguages: []
|
||||
property var translations: ({})
|
||||
property var fallbackTranslations: ({})
|
||||
|
||||
// Signals for reactive updates
|
||||
signal languageChanged(string newLanguage)
|
||||
signal translationsLoaded
|
||||
|
||||
// Process to list directory contents
|
||||
property Process directoryScanner: Process {
|
||||
id: directoryProcess
|
||||
command: ["ls", `${Quickshell.shellDir}/Assets/Translations/`]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: stdoutCollector
|
||||
}
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
if (exitCode === 0) {
|
||||
var output = stdoutCollector.text || ""
|
||||
parseDirectoryListing(output)
|
||||
} else {
|
||||
Logger.error("I18n", `Failed to scan translation directory`)
|
||||
// Fallback to default languages
|
||||
availableLanguages = ["en"]
|
||||
detectLanguage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FileView to load translation files
|
||||
property FileView translationFile: FileView {
|
||||
id: fileView
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
var data = JSON.parse(text())
|
||||
root.translations = data
|
||||
Logger.log("I18n", `Loaded translations for "${root.langCode}"`)
|
||||
|
||||
root.isLoaded = true
|
||||
root.translationsLoaded()
|
||||
} catch (e) {
|
||||
Logger.error("I18n", `Failed to parse translation file: ${e}`)
|
||||
setLanguage("en")
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
setLanguage("en")
|
||||
Logger.error("I18n", `Failed to load translation file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// FileView to load fallback translation files
|
||||
property FileView fallbackTranslationFile: FileView {
|
||||
id: fallbackFileView
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
var data = JSON.parse(text())
|
||||
root.fallbackTranslations = data
|
||||
Logger.log("I18n", `Loaded english fallback translations`)
|
||||
} catch (e) {
|
||||
Logger.error("I18n", `Failed to parse fallback translation file: ${e}`)
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
Logger.error("I18n", `Failed to load fallback translation file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("I18n", "Service started")
|
||||
scanAvailableLanguages()
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function scanAvailableLanguages() {
|
||||
Logger.log("I18n", "Scanning for available translation files...")
|
||||
directoryScanner.running = true
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function parseDirectoryListing(output) {
|
||||
var languages = []
|
||||
|
||||
try {
|
||||
if (!output || output.trim() === "") {
|
||||
Logger.warn("I18n", "Empty directory listing output")
|
||||
availableLanguages = ["en"]
|
||||
detectLanguage()
|
||||
return
|
||||
}
|
||||
|
||||
const entries = output.trim().split('\n')
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i].trim()
|
||||
if (entry && entry.endsWith('.json')) {
|
||||
// Extract language code from filename (e.g., "en.json" -> "en")
|
||||
const langCode = entry.substring(0, entry.lastIndexOf('.json'))
|
||||
if (langCode.length >= 2 && langCode.length <= 5) {
|
||||
// Basic validation for language codes
|
||||
languages.push(langCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort languages alphabetically, but ensure "en" comes first if available
|
||||
languages.sort()
|
||||
const enIndex = languages.indexOf("en")
|
||||
if (enIndex > 0) {
|
||||
languages.splice(enIndex, 1)
|
||||
languages.unshift("en")
|
||||
}
|
||||
|
||||
if (languages.length === 0) {
|
||||
Logger.warn("I18n", "No translation files found, using fallback")
|
||||
languages = ["en"] // Fallback
|
||||
}
|
||||
|
||||
availableLanguages = languages
|
||||
Logger.log("I18n", `Found ${languages.length} available languages: ${languages.join(', ')}`)
|
||||
|
||||
// Detect language after scanning
|
||||
detectLanguage()
|
||||
} catch (e) {
|
||||
Logger.error("I18n", `Failed to parse directory listing: ${e}`)
|
||||
// Fallback to default languages
|
||||
availableLanguages = ["en"]
|
||||
detectLanguage()
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function detectLanguage() {
|
||||
Logger.log("I18n", `detectLanguage() called. Available languages: [${availableLanguages.join(', ')}]`)
|
||||
|
||||
if (availableLanguages.length === 0) {
|
||||
Logger.warn("I18n", "No available languages found")
|
||||
return
|
||||
}
|
||||
|
||||
if (debug && debugForceLanguage !== "") {
|
||||
Logger.log("I18n", `Debug mode: forcing language to "${debugForceLanguage}"`)
|
||||
if (availableLanguages.includes(debugForceLanguage)) {
|
||||
setLanguage(debugForceLanguage)
|
||||
return
|
||||
} else {
|
||||
Logger.warn("I18n", `Debug language "${debugForceLanguage}" not available in [${availableLanguages.join(', ')}]`)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect user's favorite locale - languages
|
||||
for (var i = 0; i < Qt.locale().uiLanguages.length; i++) {
|
||||
const fullUserLang = Qt.locale().uiLanguages[i]
|
||||
|
||||
// Try full code match (such as zh CN, en US)
|
||||
if (availableLanguages.includes(fullUserLang)) {
|
||||
Logger.log("I18n", `Exact match found: "${fullUserLang}"`)
|
||||
setLanguage(fullUserLang)
|
||||
return
|
||||
}
|
||||
|
||||
// If full code match fails, try short code matching (such as zh, en)
|
||||
const shortUserLang = fullUserLang.substring(0, 2)
|
||||
if (availableLanguages.includes(shortUserLang)) {
|
||||
Logger.log("I18n", `Short code match found: "${shortUserLang}" from "${fullUserLang}"`)
|
||||
setLanguage(shortUserLang)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.log("I18n", `No match for system language: "${fullUserLang}"`)
|
||||
}
|
||||
|
||||
// Fallback to first available language (preferably "en" if available)
|
||||
const fallbackLang = availableLanguages.includes("en") ? "en" : availableLanguages[0]
|
||||
setLanguage(fallbackLang)
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function setLanguage(newLangCode) {
|
||||
if (newLangCode !== langCode && availableLanguages.includes(newLangCode)) {
|
||||
langCode = newLangCode
|
||||
Logger.log("I18n", `Language set to "${langCode}"`)
|
||||
languageChanged(langCode)
|
||||
loadTranslations()
|
||||
} else if (!availableLanguages.includes(newLangCode)) {
|
||||
Logger.warn("I18n", `Language "${newLangCode}" is not available`)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function loadTranslations() {
|
||||
if (langCode === "")
|
||||
return
|
||||
|
||||
const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`
|
||||
fileView.path = filePath
|
||||
isLoaded = false
|
||||
Logger.log("I18n", `Loading translations: ${langCode}`)
|
||||
|
||||
// Only load fallback translations if we are not using english and english is available
|
||||
if (langCode !== "en" && availableLanguages.includes("en")) {
|
||||
fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Check if a translation exists
|
||||
function hasTranslation(key) {
|
||||
if (!isLoaded)
|
||||
return false
|
||||
|
||||
const keys = key.split(".")
|
||||
var value = translations
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]]
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return typeof value === "string"
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Get all translation keys (useful for debugging)
|
||||
function getAllKeys(obj, prefix) {
|
||||
if (typeof obj === "undefined")
|
||||
obj = translations
|
||||
if (typeof prefix === "undefined")
|
||||
prefix = ""
|
||||
|
||||
var keys = []
|
||||
for (var key in (obj || {})) {
|
||||
const value = obj[key]
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||||
if (typeof value === "object" && value !== null) {
|
||||
keys = keys.concat(getAllKeys(value, fullKey))
|
||||
} else if (typeof value === "string") {
|
||||
keys.push(fullKey)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Reload translations (useful for development)
|
||||
function reload() {
|
||||
Logger.log("I18n", "Reloading translations")
|
||||
loadTranslations()
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Main translation function
|
||||
function tr(key, interpolations) {
|
||||
if (typeof interpolations === "undefined")
|
||||
interpolations = {}
|
||||
|
||||
if (!isLoaded) {
|
||||
// if (debug) {
|
||||
// Logger.warn("I18n", "Translations not loaded yet")
|
||||
// }
|
||||
return key
|
||||
}
|
||||
|
||||
// Navigate nested keys (e.g., "menu.file.open")
|
||||
const keys = key.split(".")
|
||||
|
||||
// Look-up translation in the active language
|
||||
var value = translations
|
||||
var notFound = false
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]]
|
||||
} else {
|
||||
if (debug) {
|
||||
Logger.warn("I18n", `Translation key "${key}" not found`)
|
||||
}
|
||||
notFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to english if not found
|
||||
if (notFound && availableLanguages.includes("en") && langCode !== "en") {
|
||||
value = fallbackTranslations
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]]
|
||||
} else {
|
||||
// Indicate this key does not even exists in the english fallback
|
||||
return `## ${key} ##`
|
||||
}
|
||||
}
|
||||
|
||||
// Make untranslated string easy to spot
|
||||
value = `<i>${value}</i>`
|
||||
} else if (notFound) {
|
||||
// No fallback available
|
||||
return `## ${key} ##`
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
if (debug) {
|
||||
Logger.warn("I18n", `Translation key "${key}" is not a string`)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Handle interpolations (e.g., "Hello {name}!")
|
||||
var result = value
|
||||
for (var placeholder in interpolations) {
|
||||
const regex = new RegExp(`\\{${placeholder}\\}`, 'g')
|
||||
result = result.replace(regex, interpolations[placeholder])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Plural translation function
|
||||
function trp(key, count, defaultSingular, defaultPlural, interpolations) {
|
||||
if (typeof defaultSingular === "undefined")
|
||||
defaultSingular = ""
|
||||
if (typeof defaultPlural === "undefined")
|
||||
defaultPlural = ""
|
||||
if (typeof interpolations === "undefined")
|
||||
interpolations = {}
|
||||
|
||||
const pluralKey = count === 1 ? key : `${key}_plural`
|
||||
const defaultValue = count === 1 ? defaultSingular : defaultPlural
|
||||
|
||||
// Merge interpolations with count (QML doesn't support spread operator)
|
||||
var finalInterpolations = {
|
||||
"count": count
|
||||
}
|
||||
for (var prop in interpolations) {
|
||||
finalInterpolations[prop] = interpolations[prop]
|
||||
}
|
||||
|
||||
return tr(pluralKey, finalInterpolations)
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,85 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: icons
|
||||
id: root
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable"
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback)
|
||||
if (p && p !== "")
|
||||
return p
|
||||
}
|
||||
} catch (e) {
|
||||
// Expose the font family name for easy access
|
||||
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
|
||||
readonly property string defaultIcon: TablerIcons.defaultIcon
|
||||
readonly property var icons: TablerIcons.icons
|
||||
readonly property var aliases: TablerIcons.aliases
|
||||
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
|
||||
|
||||
// ignore and fall back
|
||||
}
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
|
||||
} catch (e2) {
|
||||
return ""
|
||||
// Current active font loader
|
||||
property FontLoader currentFontLoader: null
|
||||
property int fontVersion: 0
|
||||
|
||||
// Create a unique cache-busting path
|
||||
readonly property string cacheBustingPath: Quickshell.shellDir + fontPath + "?v=" + fontVersion + "&t=" + Date.now()
|
||||
|
||||
// Signal emitted when font is reloaded
|
||||
signal fontReloaded
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("Icons", "Service started")
|
||||
loadFontWithCacheBusting()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onReloadCompleted() {
|
||||
Logger.log("Icons", "Quickshell reload completed - forcing font reload")
|
||||
reloadFont()
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve icon path for a DesktopEntries appId - safe on missing entries
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable"
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback)
|
||||
try {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return iconFromName(fallback, fallback)
|
||||
const entry = DesktopEntries.byId(appId)
|
||||
const name = entry && entry.icon ? entry.icon : ""
|
||||
return iconFromName(name || fallback, fallback)
|
||||
} catch (e) {
|
||||
return iconFromName(fallback, fallback)
|
||||
// ---------------------------------------
|
||||
function get(iconName) {
|
||||
// Check in aliases first
|
||||
if (aliases[iconName] !== undefined) {
|
||||
iconName = aliases[iconName]
|
||||
}
|
||||
|
||||
// Find the appropriate codepoint
|
||||
return icons[iconName]
|
||||
}
|
||||
|
||||
function loadFontWithCacheBusting() {
|
||||
Logger.log("Icons", "Loading font with cache busting")
|
||||
|
||||
// Destroy old loader first
|
||||
if (currentFontLoader) {
|
||||
currentFontLoader.destroy()
|
||||
currentFontLoader = null
|
||||
}
|
||||
|
||||
// Create new loader with cache-busting URL
|
||||
currentFontLoader = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
FontLoader {
|
||||
source: "${cacheBustingPath}"
|
||||
}
|
||||
`, root, "dynamicFontLoader_" + fontVersion)
|
||||
|
||||
// Connect to the new loader's status changes
|
||||
currentFontLoader.statusChanged.connect(function () {
|
||||
if (currentFontLoader.status === FontLoader.Ready) {
|
||||
Logger.log("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")")
|
||||
fontReloaded()
|
||||
} else if (currentFontLoader.status === FontLoader.Error) {
|
||||
Logger.error("Icons", "Font failed to load (version " + fontVersion + ")")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function reloadFont() {
|
||||
Logger.log("Icons", "Forcing font reload...")
|
||||
fontVersion++
|
||||
loadFontWithCacheBusting()
|
||||
}
|
||||
}
|
||||
|
||||
205
Commons/KeyboardLayout.qml
Normal file
@@ -0,0 +1,205 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Comprehensive language name to ISO code mapping
|
||||
property var languageMap: {
|
||||
"english"// English variants
|
||||
: "us",
|
||||
"american": "us",
|
||||
"united states": "us",
|
||||
"us english": "us",
|
||||
"british": "gb",
|
||||
"uk": "ua",
|
||||
"united kingdom"// FIXED: Ukrainian language code should map to Ukraine
|
||||
: "gb",
|
||||
"english (uk)": "gb",
|
||||
"canadian": "ca",
|
||||
"canada": "ca",
|
||||
"canadian english": "ca",
|
||||
"australian": "au",
|
||||
"australia": "au",
|
||||
"swedish"// Nordic countries
|
||||
: "se",
|
||||
"svenska": "se",
|
||||
"sweden": "se",
|
||||
"norwegian": "no",
|
||||
"norsk": "no",
|
||||
"norway": "no",
|
||||
"danish": "dk",
|
||||
"dansk": "dk",
|
||||
"denmark": "dk",
|
||||
"finnish": "fi",
|
||||
"suomi": "fi",
|
||||
"finland": "fi",
|
||||
"icelandic": "is",
|
||||
"íslenska": "is",
|
||||
"iceland": "is",
|
||||
"german"// Western/Central European Germanic
|
||||
: "de",
|
||||
"deutsch": "de",
|
||||
"germany": "de",
|
||||
"austrian": "at",
|
||||
"austria": "at",
|
||||
"österreich": "at",
|
||||
"swiss": "ch",
|
||||
"switzerland": "ch",
|
||||
"schweiz": "ch",
|
||||
"suisse": "ch",
|
||||
"dutch": "nl",
|
||||
"nederlands": "nl",
|
||||
"netherlands": "nl",
|
||||
"holland": "nl",
|
||||
"belgian": "be",
|
||||
"belgium": "be",
|
||||
"belgië": "be",
|
||||
"belgique": "be",
|
||||
"french"// Romance languages (Western/Southern Europe)
|
||||
: "fr",
|
||||
"français": "fr",
|
||||
"france": "fr",
|
||||
"canadian french": "ca",
|
||||
"spanish": "es",
|
||||
"español": "es",
|
||||
"spain": "es",
|
||||
"castilian": "es",
|
||||
"italian": "it",
|
||||
"italiano": "it",
|
||||
"italy": "it",
|
||||
"portuguese": "pt",
|
||||
"português": "pt",
|
||||
"portugal": "pt",
|
||||
"catalan": "ad",
|
||||
"català": "ad",
|
||||
"andorra": "ad",
|
||||
"romanian"// Eastern European Romance
|
||||
: "ro",
|
||||
"română": "ro",
|
||||
"romania": "ro",
|
||||
"russian"// Slavic languages (Eastern Europe)
|
||||
: "ru",
|
||||
"русский": "ru",
|
||||
"russia": "ru",
|
||||
"polish": "pl",
|
||||
"polski": "pl",
|
||||
"poland": "pl",
|
||||
"czech": "cz",
|
||||
"čeština": "cz",
|
||||
"czech republic": "cz",
|
||||
"slovak": "sk",
|
||||
"slovenčina": "sk",
|
||||
"slovakia": "sk",
|
||||
"uk": "ua",
|
||||
"ukrainian"// Ukrainian language code
|
||||
: "ua",
|
||||
"українська": "ua",
|
||||
"ukraine": "ua",
|
||||
"bulgarian": "bg",
|
||||
"български": "bg",
|
||||
"bulgaria": "bg",
|
||||
"serbian": "rs",
|
||||
"srpski": "rs",
|
||||
"serbia": "rs",
|
||||
"croatian": "hr",
|
||||
"hrvatski": "hr",
|
||||
"croatia": "hr",
|
||||
"slovenian": "si",
|
||||
"slovenščina": "si",
|
||||
"slovenia": "si",
|
||||
"bosnian": "ba",
|
||||
"bosanski": "ba",
|
||||
"bosnia": "ba",
|
||||
"macedonian": "mk",
|
||||
"македонски": "mk",
|
||||
"macedonia": "mk",
|
||||
"irish"// Celtic languages (Western Europe)
|
||||
: "ie",
|
||||
"gaeilge": "ie",
|
||||
"ireland": "ie",
|
||||
"welsh": "gb",
|
||||
"cymraeg": "gb",
|
||||
"wales": "gb",
|
||||
"scottish": "gb",
|
||||
"gàidhlig": "gb",
|
||||
"scotland": "gb",
|
||||
"estonian"// Baltic languages (Northern Europe)
|
||||
: "ee",
|
||||
"eesti": "ee",
|
||||
"estonia": "ee",
|
||||
"latvian": "lv",
|
||||
"latviešu": "lv",
|
||||
"latvia": "lv",
|
||||
"lithuanian": "lt",
|
||||
"lietuvių": "lt",
|
||||
"lithuania": "lt",
|
||||
"hungarian"// Other European languages
|
||||
: "hu",
|
||||
"magyar": "hu",
|
||||
"hungary": "hu",
|
||||
"greek": "gr",
|
||||
"ελληνικά": "gr",
|
||||
"greece": "gr",
|
||||
"albanian": "al",
|
||||
"shqip": "al",
|
||||
"albania": "al",
|
||||
"maltese": "mt",
|
||||
"malti": "mt",
|
||||
"malta": "mt",
|
||||
"turkish"// West/Southwest Asian languages
|
||||
: "tr",
|
||||
"türkçe": "tr",
|
||||
"turkey": "tr",
|
||||
"arabic": "ar",
|
||||
"العربية": "ar",
|
||||
"arab": "ar",
|
||||
"hebrew": "il",
|
||||
"עברית": "il",
|
||||
"israel": "il",
|
||||
"brazilian"// South American languages
|
||||
: "br",
|
||||
"brazilian portuguese": "br",
|
||||
"brasil": "br",
|
||||
"brazil": "br",
|
||||
"japanese"// East Asian languages
|
||||
: "jp",
|
||||
"日本語": "jp",
|
||||
"japan": "jp",
|
||||
"korean": "kr",
|
||||
"한국어": "kr",
|
||||
"korea": "kr",
|
||||
"south korea": "kr",
|
||||
"chinese": "cn",
|
||||
"中文": "cn",
|
||||
"china": "cn",
|
||||
"simplified chinese": "cn",
|
||||
"traditional chinese": "tw",
|
||||
"taiwan": "tw",
|
||||
"繁體中文": "tw",
|
||||
"thai"// Southeast Asian languages
|
||||
: "th",
|
||||
"ไทย": "th",
|
||||
"thailand": "th",
|
||||
"vietnamese": "vn",
|
||||
"tiếng việt": "vn",
|
||||
"vietnam": "vn",
|
||||
"hindi"// South Asian languages
|
||||
: "in",
|
||||
"हिन्दी": "in",
|
||||
"india": "in",
|
||||
"afrikaans"// African languages
|
||||
: "za",
|
||||
"south africa": "za",
|
||||
"south african": "za",
|
||||
"qwerty"// Layout variants
|
||||
: "us",
|
||||
"dvorak": "us",
|
||||
"colemak": "us",
|
||||
"workman": "us",
|
||||
"azerty": "fr",
|
||||
"norman": "fr",
|
||||
"qwertz": "de"
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function _getStackTrace() {
|
||||
try {
|
||||
throw new Error("Stack trace")
|
||||
} catch (e) {
|
||||
return e.stack
|
||||
}
|
||||
}
|
||||
|
||||
function log(...args) {
|
||||
var msg = _formatMessage(...args)
|
||||
console.log(msg)
|
||||
@@ -31,4 +39,20 @@ Singleton {
|
||||
var msg = _formatMessage(...args)
|
||||
console.error(msg)
|
||||
}
|
||||
|
||||
function callStack() {
|
||||
var stack = _getStackTrace()
|
||||
Logger.log("Debug", "--------------------------")
|
||||
Logger.log("Debug", "Current call stack")
|
||||
// Split the stack into lines and log each one
|
||||
var stackLines = stack.split('\n')
|
||||
for (var i = 0; i < stackLines.length; i++) {
|
||||
var line = stackLines[i].trim() // Remove leading/trailing whitespace
|
||||
if (line.length > 0) {
|
||||
// Only log non-empty lines
|
||||
Logger.log("Debug", `- ${line}`)
|
||||
}
|
||||
}
|
||||
Logger.log("Debug", "--------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,32 +5,369 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import "../Helpers/QtObj2JS.js" as QtObj2JS
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Used to access via Settings.data.xxx.yyy
|
||||
readonly property alias data: adapter
|
||||
property bool isLoaded: false
|
||||
property bool directoriesCreated: false
|
||||
|
||||
// Define our app directories
|
||||
// Default config directory: ~/.config/noctalia
|
||||
// Default cache directory: ~/.cache/noctalia
|
||||
property string shellName: "noctalia"
|
||||
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME")
|
||||
|| Quickshell.env(
|
||||
"HOME") + "/.config") + "/" + shellName + "/"
|
||||
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env(
|
||||
"HOME") + "/.cache") + "/" + shellName + "/"
|
||||
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
|
||||
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/"
|
||||
property string cacheDirImages: cacheDir + "images/"
|
||||
|
||||
property string cacheDirImagesWallpapers: cacheDir + "images/wallpapers/"
|
||||
property string cacheDirImagesNotifications: cacheDir + "images/notifications/"
|
||||
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json")
|
||||
|
||||
property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png")
|
||||
property string defaultLocation: "Tokyo"
|
||||
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
|
||||
|
||||
property string defaultAvatar: Quickshell.env("HOME") + "/.face"
|
||||
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
|
||||
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
|
||||
|
||||
// Used to access via Settings.data.xxx.yyy
|
||||
property alias data: adapter
|
||||
// Signal emitted when settings are loaded after startupcale changes
|
||||
signal settingsLoaded
|
||||
|
||||
// Flag to prevent unnecessary wallpaper calls during reloads
|
||||
property bool isInitialLoad: true
|
||||
// -----------------------------------------------------
|
||||
// -----------------------------------------------------
|
||||
// Ensure directories exist before FileView tries to read files
|
||||
Component.onCompleted: {
|
||||
// ensure settings dir exists
|
||||
Quickshell.execDetached(["mkdir", "-p", configDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDir])
|
||||
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesWallpapers])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesNotifications])
|
||||
|
||||
// Mark directories as created and trigger file loading
|
||||
directoriesCreated = true
|
||||
|
||||
// This should only be activated once when the settings structure has changed
|
||||
// Then it should be commented out again, regular users don't need to generate
|
||||
// default settings on every start
|
||||
// TODO: automate this someday!
|
||||
// generateDefaultSettings()
|
||||
|
||||
// Patch-in the local default, resolved to user's home
|
||||
adapter.general.avatarImage = defaultAvatar
|
||||
adapter.screenRecorder.directory = defaultVideosDirectory
|
||||
adapter.wallpaper.directory = defaultWallpapersDirectory
|
||||
|
||||
// Set the adapter to the settingsFileView to trigger the real settings load
|
||||
settingsFileView.adapter = adapter
|
||||
}
|
||||
|
||||
// Don't write settings to disk immediately
|
||||
// This avoid excessive IO when a variable changes rapidly (ex: sliders)
|
||||
Timer {
|
||||
id: saveTimer
|
||||
running: false
|
||||
interval: 1000
|
||||
onTriggered: {
|
||||
settingsFileView.writeAdapter()
|
||||
// Write to fallback location if set
|
||||
if (Quickshell.env("NOCTALIA_SETTINGS_FALLBACK")) {
|
||||
settingsFallbackFileView.writeAdapter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: settingsFileView
|
||||
path: directoriesCreated ? settingsFile : undefined
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onAdapterUpdated: saveTimer.start()
|
||||
|
||||
// Trigger initial load when path changes from empty to actual path
|
||||
onPathChanged: {
|
||||
if (path !== undefined) {
|
||||
reload()
|
||||
}
|
||||
}
|
||||
onLoaded: function () {
|
||||
if (!isLoaded) {
|
||||
Logger.log("Settings", "Settings loaded")
|
||||
|
||||
upgradeSettingsData()
|
||||
validateMonitorConfigurations()
|
||||
isLoaded = true
|
||||
|
||||
// Emit the signal
|
||||
root.settingsLoaded()
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
if (error.toString().includes("No such file") || error === 2) {
|
||||
// File doesn't exist, create it with default values
|
||||
writeAdapter()
|
||||
// Also write to fallback if set
|
||||
if (Quickshell.env("NOCTALIA_SETTINGS_FALLBACK")) {
|
||||
settingsFallbackFileView.writeAdapter()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback FileView for writing settings to alternate location
|
||||
FileView {
|
||||
id: settingsFallbackFileView
|
||||
path: Quickshell.env("NOCTALIA_SETTINGS_FALLBACK") || ""
|
||||
adapter: Quickshell.env("NOCTALIA_SETTINGS_FALLBACK") ? adapter : null
|
||||
printErrors: false
|
||||
watchChanges: false
|
||||
}
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property int settingsVersion: 12
|
||||
|
||||
// bar
|
||||
property JsonObject bar: JsonObject {
|
||||
property string position: "top" // "top", "bottom", "left", or "right"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> monitors: []
|
||||
property string density: "default" // "compact", "default", "comfortable"
|
||||
property bool showCapsule: true
|
||||
|
||||
// Floating bar settings
|
||||
property bool floating: false
|
||||
property real marginVertical: 0.25
|
||||
property real marginHorizontal: 0.25
|
||||
|
||||
// Widget configuration for modular bar system
|
||||
property JsonObject widgets
|
||||
widgets: JsonObject {
|
||||
property list<var> left: [{
|
||||
"id": "SystemMonitor"
|
||||
}, {
|
||||
"id": "ActiveWindow"
|
||||
}, {
|
||||
"id": "MediaMini"
|
||||
}]
|
||||
property list<var> center: [{
|
||||
"id": "Workspace"
|
||||
}]
|
||||
property list<var> right: [{
|
||||
"id": "ScreenRecorder"
|
||||
}, {
|
||||
"id": "Tray"
|
||||
}, {
|
||||
"id": "NotificationHistory"
|
||||
}, {
|
||||
"id": "WiFi"
|
||||
}, {
|
||||
"id": "Bluetooth"
|
||||
}, {
|
||||
"id": "Battery"
|
||||
}, {
|
||||
"id": "Volume"
|
||||
}, {
|
||||
"id": "Brightness"
|
||||
}, {
|
||||
"id": "Clock"
|
||||
}, {
|
||||
"id": "ControlCenter"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// general
|
||||
property JsonObject general: JsonObject {
|
||||
property string avatarImage: ""
|
||||
property bool dimDesktop: true
|
||||
property bool showScreenCorners: false
|
||||
property bool forceBlackScreenCorners: false
|
||||
property real radiusRatio: 1.0
|
||||
property real screenRadiusRatio: 1.0
|
||||
property real animationSpeed: 1.0
|
||||
property bool animationDisabled: false
|
||||
}
|
||||
|
||||
// location
|
||||
property JsonObject location: JsonObject {
|
||||
property string name: defaultLocation
|
||||
property bool useFahrenheit: false
|
||||
property bool use12hourFormat: false
|
||||
property bool showWeekNumberInCalendar: false
|
||||
}
|
||||
|
||||
// screen recorder
|
||||
property JsonObject screenRecorder: JsonObject {
|
||||
property string directory: ""
|
||||
property int frameRate: 60
|
||||
property string audioCodec: "opus"
|
||||
property string videoCodec: "h264"
|
||||
property string quality: "very_high"
|
||||
property string colorRange: "limited"
|
||||
property bool showCursor: true
|
||||
property string audioSource: "default_output"
|
||||
property string videoSource: "portal"
|
||||
}
|
||||
|
||||
// wallpaper
|
||||
property JsonObject wallpaper: JsonObject {
|
||||
property bool enabled: true
|
||||
property string directory: ""
|
||||
property bool enableMultiMonitorDirectories: false
|
||||
property bool setWallpaperOnAllMonitors: true
|
||||
property string fillMode: "crop"
|
||||
property color fillColor: "#000000"
|
||||
property bool randomEnabled: false
|
||||
property int randomIntervalSec: 300 // 5 min
|
||||
property int transitionDuration: 1500 // 1500 ms
|
||||
property string transitionType: "random"
|
||||
property real transitionEdgeSmoothness: 0.05
|
||||
property list<var> monitors: []
|
||||
}
|
||||
|
||||
// applauncher
|
||||
property JsonObject appLauncher: JsonObject {
|
||||
property bool enableClipboardHistory: false
|
||||
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
|
||||
property string position: "center"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> pinnedExecs: []
|
||||
property bool useApp2Unit: false
|
||||
property bool sortByMostUsed: true
|
||||
property string terminalCommand: "xterm -e"
|
||||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property real backgroundOpacity: 1.0
|
||||
property real floatingRatio: 1.0
|
||||
property bool onlySameOutput: true
|
||||
property list<string> monitors: []
|
||||
// Desktop entry IDs pinned to the dock (e.g., "org.kde.konsole", "firefox.desktop")
|
||||
property list<string> pinnedApps: []
|
||||
}
|
||||
|
||||
// network
|
||||
property JsonObject network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
}
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool doNotDisturb: false
|
||||
property list<string> monitors: []
|
||||
property string location: "top_right"
|
||||
property bool alwaysOnTop: false
|
||||
property real lastSeenTs: 0
|
||||
property bool respectExpireTimeout: false
|
||||
property int lowUrgencyDuration: 3
|
||||
property int normalUrgencyDuration: 8
|
||||
property int criticalUrgencyDuration: 15
|
||||
}
|
||||
|
||||
// on-screen display
|
||||
property JsonObject osd: JsonObject {
|
||||
property bool enabled: true
|
||||
property string location: "top_right"
|
||||
property list<string> monitors: []
|
||||
property int autoHideMs: 2000
|
||||
}
|
||||
|
||||
// audio
|
||||
property JsonObject audio: JsonObject {
|
||||
property int volumeStep: 5
|
||||
property bool volumeOverdrive: false
|
||||
property int cavaFrameRate: 60
|
||||
property string visualizerType: "linear"
|
||||
property list<string> mprisBlacklist: []
|
||||
property string preferredPlayer: ""
|
||||
}
|
||||
|
||||
// ui
|
||||
property JsonObject ui: JsonObject {
|
||||
property string fontDefault: "Roboto"
|
||||
property string fontFixed: "DejaVu Sans Mono"
|
||||
property real fontDefaultScale: 1.0
|
||||
property real fontFixedScale: 1.0
|
||||
property list<var> monitorsScaling: []
|
||||
property bool idleInhibitorEnabled: false
|
||||
}
|
||||
|
||||
// brightness
|
||||
property JsonObject brightness: JsonObject {
|
||||
property int brightnessStep: 5
|
||||
}
|
||||
|
||||
property JsonObject colorSchemes: JsonObject {
|
||||
property bool useWallpaperColors: false
|
||||
property string predefinedScheme: "Noctalia (default)"
|
||||
property bool darkMode: true
|
||||
property string matugenSchemeType: "scheme-fruit-salad"
|
||||
}
|
||||
|
||||
// matugen templates toggles
|
||||
property JsonObject matugen: JsonObject {
|
||||
// Per-template flags to control dynamic config generation
|
||||
property bool gtk4: false
|
||||
property bool gtk3: false
|
||||
property bool qt6: false
|
||||
property bool qt5: false
|
||||
property bool kitty: false
|
||||
property bool ghostty: false
|
||||
property bool foot: false
|
||||
property bool fuzzel: false
|
||||
property bool vesktop: false
|
||||
property bool pywalfox: false
|
||||
property bool enableUserTemplates: false
|
||||
}
|
||||
|
||||
// night light
|
||||
property JsonObject nightLight: JsonObject {
|
||||
property bool enabled: false
|
||||
property bool forced: false
|
||||
property bool autoSchedule: true
|
||||
property string nightTemp: "4000"
|
||||
property string dayTemp: "6500"
|
||||
property string manualSunrise: "06:30"
|
||||
property string manualSunset: "18:30"
|
||||
}
|
||||
|
||||
// hooks
|
||||
property JsonObject hooks: JsonObject {
|
||||
property bool enabled: false
|
||||
property string wallpaperChange: ""
|
||||
property string darkModeChange: ""
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Generate default settings at the root of the repo
|
||||
function generateDefaultSettings() {
|
||||
try {
|
||||
Logger.log("Settings", "Generating settings-default.json")
|
||||
|
||||
// Prepare a clean JSON
|
||||
var plainAdapter = QtObj2JS.qtObjectToPlainObject(adapter)
|
||||
var jsonData = JSON.stringify(plainAdapter, null, 2)
|
||||
|
||||
var defaultPath = Quickshell.shellDir + "/Assets/settings-default.json"
|
||||
|
||||
// Encode transfer it has base64 to avoid any escaping issue
|
||||
var base64Data = Qt.btoa(jsonData)
|
||||
Quickshell.execDetached(["sh", "-c", `echo "${base64Data}" | base64 -d > "${defaultPath}"`])
|
||||
} catch (error) {
|
||||
Logger.error("Settings", "Failed to generate default settings file: " + error)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Function to validate monitor configurations
|
||||
function validateMonitorConfigurations() {
|
||||
var availableScreenNames = []
|
||||
@@ -51,205 +388,149 @@ Singleton {
|
||||
}
|
||||
}
|
||||
if (!hasValidBarMonitor) {
|
||||
Logger.log("Settings",
|
||||
"No configured bar monitors found on system, clearing bar monitor list to show on all screens")
|
||||
Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
|
||||
adapter.bar.monitors = []
|
||||
} else {
|
||||
Logger.log("Settings", "Found valid bar monitors, keeping configuration")
|
||||
|
||||
//Logger.log("Settings", "Found valid bar monitors, keeping configuration")
|
||||
}
|
||||
} else {
|
||||
Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Component.onCompleted: {
|
||||
|
||||
// ensure settings dir exists
|
||||
Quickshell.execDetached(["mkdir", "-p", configDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
|
||||
//Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
|
||||
}
|
||||
}
|
||||
|
||||
// Don't write settings to disk immediately
|
||||
// This avoid excessive IO when a variable changes rapidly (ex: sliders)
|
||||
Timer {
|
||||
id: saveTimer
|
||||
running: false
|
||||
interval: 1000
|
||||
onTriggered: settingsFileView.writeAdapter()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: settingsFileView
|
||||
path: settingsFile
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onAdapterUpdated: saveTimer.start()
|
||||
Component.onCompleted: function () {
|
||||
reload()
|
||||
// -----------------------------------------------------
|
||||
// If the settings structure has changed, ensure
|
||||
// backward compatibility by upgrading the settings
|
||||
function upgradeSettingsData() {
|
||||
// Wait for BarWidgetRegistry to be ready
|
||||
if (!BarWidgetRegistry.widgets || Object.keys(BarWidgetRegistry.widgets).length === 0) {
|
||||
Logger.warn("Settings", "BarWidgetRegistry not ready, deferring upgrade")
|
||||
Qt.callLater(upgradeSettingsData)
|
||||
return
|
||||
}
|
||||
onLoaded: function () {
|
||||
Qt.callLater(function () {
|
||||
if (isInitialLoad) {
|
||||
Logger.log("Settings", "OnLoaded")
|
||||
// Only set wallpaper on initial load, not on reloads
|
||||
if (adapter.wallpaper.current !== "") {
|
||||
Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current)
|
||||
WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true)
|
||||
|
||||
const sections = ["left", "center", "right"]
|
||||
|
||||
// -----------------
|
||||
// 1st. convert old widget id to new id
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
|
||||
switch (widget.id) {
|
||||
case "DarkModeToggle":
|
||||
widget.id = "DarkMode"
|
||||
break
|
||||
case "PowerToggle":
|
||||
widget.id = "SessionMenu"
|
||||
break
|
||||
case "ScreenRecorderIndicator":
|
||||
widget.id = "ScreenRecorder"
|
||||
break
|
||||
case "SidePanelToggle":
|
||||
widget.id = "ControlCenter"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 2nd. remove any non existing widget type
|
||||
var removedWidget = false
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
const widgets = adapter.bar.widgets[sectionName]
|
||||
// Iterate backward through the widgets array, so it does not break when removing a widget
|
||||
for (var i = widgets.length - 1; i >= 0; i--) {
|
||||
var widget = widgets[i]
|
||||
if (!BarWidgetRegistry.hasWidget(widget.id)) {
|
||||
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
|
||||
widgets.splice(i, 1)
|
||||
removedWidget = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 3nd. upgrade widget settings
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
|
||||
// Check if widget registry supports user settings, if it does not, then there is nothing to do
|
||||
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
|
||||
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (upgradeWidget(widget)) {
|
||||
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 4th. safety check
|
||||
// if a widget was deleted, ensure we still have a control center
|
||||
if (removedWidget) {
|
||||
var gotControlCenter = false
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
if (widget.id === "ControlCenter") {
|
||||
gotControlCenter = true
|
||||
break
|
||||
}
|
||||
|
||||
// Validate monitor configurations, only once
|
||||
// if none of the configured monitors exist, clear the lists
|
||||
validateMonitorConfigurations()
|
||||
}
|
||||
|
||||
isInitialLoad = false
|
||||
})
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
if (error.toString().includes("No such file") || error === 2)
|
||||
// File doesn't exist, create it with default values
|
||||
writeAdapter()
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
// bar
|
||||
property JsonObject bar: JsonObject {
|
||||
property string position: "top" // Possible values: "top", "bottom"
|
||||
property bool showActiveWindowIcon: true
|
||||
property bool alwaysShowBatteryPercentage: false
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> monitors: []
|
||||
|
||||
// Widget configuration for modular bar system
|
||||
property JsonObject widgets
|
||||
widgets: JsonObject {
|
||||
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
|
||||
property list<string> center: ["Workspace"]
|
||||
property list<string> right: ["ScreenRecorderIndicator", "Tray", "ArchUpdater", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
|
||||
}
|
||||
}
|
||||
|
||||
// general
|
||||
property JsonObject general: JsonObject {
|
||||
property string avatarImage: defaultAvatar
|
||||
property bool dimDesktop: false
|
||||
property bool showScreenCorners: false
|
||||
property real radiusRatio: 1.0
|
||||
}
|
||||
|
||||
// location
|
||||
property JsonObject location: JsonObject {
|
||||
property string name: "Tokyo"
|
||||
property bool useFahrenheit: false
|
||||
property bool reverseDayMonth: false
|
||||
property bool use12HourClock: false
|
||||
property bool showDateWithClock: false
|
||||
}
|
||||
|
||||
// screen recorder
|
||||
property JsonObject screenRecorder: JsonObject {
|
||||
property string directory: "~/Videos"
|
||||
property int frameRate: 60
|
||||
property string audioCodec: "opus"
|
||||
property string videoCodec: "h264"
|
||||
property string quality: "very_high"
|
||||
property string colorRange: "limited"
|
||||
property bool showCursor: true
|
||||
property string audioSource: "default_output"
|
||||
property string videoSource: "portal"
|
||||
}
|
||||
|
||||
// wallpaper
|
||||
property JsonObject wallpaper: JsonObject {
|
||||
property string directory: "/usr/share/wallpapers"
|
||||
property string current: ""
|
||||
property bool isRandom: false
|
||||
property int randomInterval: 300
|
||||
property JsonObject swww
|
||||
|
||||
onDirectoryChanged: WallpaperService.listWallpapers()
|
||||
onIsRandomChanged: WallpaperService.toggleRandomWallpaper()
|
||||
onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer()
|
||||
|
||||
swww: JsonObject {
|
||||
property bool enabled: false
|
||||
property string resizeMethod: "crop"
|
||||
property int transitionFps: 60
|
||||
property string transitionType: "random"
|
||||
property real transitionDuration: 1.1
|
||||
}
|
||||
}
|
||||
|
||||
// applauncher
|
||||
property JsonObject appLauncher: JsonObject {
|
||||
// When disabled, Launcher hides clipboard command and ignores cliphist
|
||||
property bool enableClipboardHistory: true
|
||||
// Position: center, top_left, top_right, bottom_left, bottom_right
|
||||
property string position: "center"
|
||||
property list<string> pinnedExecs: []
|
||||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
// network
|
||||
property JsonObject network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
property bool bluetoothEnabled: true
|
||||
}
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications: JsonObject {
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
// audio
|
||||
property JsonObject audio: JsonObject {
|
||||
property bool showMiniplayerAlbumArt: false
|
||||
property bool showMiniplayerCava: false
|
||||
property string visualizerType: "linear"
|
||||
property int volumeStep: 5
|
||||
property int cavaFrameRate: 60
|
||||
}
|
||||
|
||||
// ui
|
||||
property JsonObject ui: JsonObject {
|
||||
property string fontDefault: "Roboto" // Default font for all text
|
||||
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
|
||||
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
|
||||
|
||||
// Legacy compatibility
|
||||
property string fontFamily: fontDefault // Keep for backward compatibility
|
||||
|
||||
// Idle inhibitor state
|
||||
property bool idleInhibitorEnabled: false
|
||||
}
|
||||
|
||||
// Scaling (not stored inside JsonObject, or it crashes)
|
||||
property var monitorsScaling: {
|
||||
|
||||
}
|
||||
|
||||
// brightness
|
||||
property JsonObject brightness: JsonObject {
|
||||
property int brightnessStep: 5
|
||||
}
|
||||
|
||||
property JsonObject colorSchemes: JsonObject {
|
||||
property bool useWallpaperColors: false
|
||||
property string predefinedScheme: ""
|
||||
property bool darkMode: true
|
||||
// External app theming (GTK & Qt)
|
||||
property bool themeApps: false
|
||||
if (!gotControlCenter) {
|
||||
//const obj = JSON.parse('{"id": "ControlCenter"}');
|
||||
adapter.bar.widgets["right"].push(({
|
||||
"id": "ControlCenter"
|
||||
}))
|
||||
Logger.warn("Settings", "Added a ControlCenter widget to the right section")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
function upgradeWidget(widget) {
|
||||
// Backup the widget definition before altering
|
||||
const widgetBefore = JSON.stringify(widget)
|
||||
|
||||
// Get all existing custom settings keys
|
||||
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
|
||||
|
||||
// Delete deprecated user settings from the wiget
|
||||
for (const k of Object.keys(widget)) {
|
||||
if (k === "id" || k === "allowUserSettings") {
|
||||
continue
|
||||
}
|
||||
if (!keys.includes(k)) {
|
||||
delete widget[k]
|
||||
}
|
||||
}
|
||||
|
||||
// Inject missing default setting (metaData) from BarWidgetRegistry
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
const k = keys[i]
|
||||
if (k === "id" || k === "allowUserSettings") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (widget[k] === undefined) {
|
||||
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
|
||||
}
|
||||
}
|
||||
|
||||
// Compare settings, to detect if something has been upgraded
|
||||
const widgetAfter = JSON.stringify(widget)
|
||||
return (widgetAfter !== widgetBefore)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ Singleton {
|
||||
*/
|
||||
|
||||
// Font size
|
||||
property real fontSizeXXS: 8
|
||||
property real fontSizeXS: 9
|
||||
property real fontSizeS: 10
|
||||
property real fontSizeM: 11
|
||||
@@ -28,11 +29,15 @@ Singleton {
|
||||
property int fontWeightBold: 700
|
||||
|
||||
// Radii
|
||||
property int radiusXXS: 4 * Settings.data.general.radiusRatio
|
||||
property int radiusXS: 8 * Settings.data.general.radiusRatio
|
||||
property int radiusS: 12 * Settings.data.general.radiusRatio
|
||||
property int radiusM: 16 * Settings.data.general.radiusRatio
|
||||
property int radiusL: 20 * Settings.data.general.radiusRatio
|
||||
|
||||
//screen Radii
|
||||
property int screenRadius: 20 * Settings.data.general.screenRadiusRatio
|
||||
|
||||
// Border
|
||||
property int borderS: 1
|
||||
property int borderM: 2
|
||||
@@ -55,18 +60,41 @@ Singleton {
|
||||
property real opacityFull: 1.0
|
||||
|
||||
// Animation duration (ms)
|
||||
property int animationFast: 150
|
||||
property int animationNormal: 300
|
||||
property int animationSlow: 450
|
||||
|
||||
// Dimensions
|
||||
property int barHeight: 36
|
||||
property int capsuleHeight: (barHeight * 0.73)
|
||||
property int baseWidgetSize: 32
|
||||
property int sliderWidth: 200
|
||||
property int animationFast: Settings.data.general.animationDisabled ? 0 : Math.round(150 / Settings.data.general.animationSpeed)
|
||||
property int animationNormal: Settings.data.general.animationDisabled ? 0 : Math.round(300 / Settings.data.general.animationSpeed)
|
||||
property int animationSlow: Settings.data.general.animationDisabled ? 0 : Math.round(450 / Settings.data.general.animationSpeed)
|
||||
property int animationSlowest: Settings.data.general.animationDisabled ? 0 : Math.round(750 / Settings.data.general.animationSpeed)
|
||||
|
||||
// Delays
|
||||
property int tooltipDelay: 300
|
||||
property int tooltipDelayLong: 1200
|
||||
property int pillDelay: 500
|
||||
|
||||
// Settings widgets base size
|
||||
property real baseWidgetSize: 33
|
||||
property real sliderWidth: 200
|
||||
|
||||
// Bar Dimensions
|
||||
property real barHeight: {
|
||||
if (Settings.data.bar.density === "compact") {
|
||||
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 27 : 25
|
||||
}
|
||||
if (Settings.data.bar.density === "default") {
|
||||
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 33 : 31
|
||||
}
|
||||
if (Settings.data.bar.density === "comfortable") {
|
||||
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37
|
||||
}
|
||||
}
|
||||
property real capsuleHeight: {
|
||||
if (Settings.data.bar.density === "compact") {
|
||||
return barHeight * 0.85
|
||||
}
|
||||
if (Settings.data.bar.density === "default") {
|
||||
return barHeight * 0.82
|
||||
}
|
||||
if (Settings.data.bar.density === "comfortable") {
|
||||
return barHeight * 0.73
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6169
Commons/TablerIcons.qml
Normal file
53
Commons/ThemeIcons.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable"
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback)
|
||||
if (p && p !== "")
|
||||
return p
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// ignore and fall back
|
||||
}
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
|
||||
} catch (e2) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve icon path for a DesktopEntries appId - safe on missing entries
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable"
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback)
|
||||
try {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return iconFromName(fallback, fallback)
|
||||
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId)
|
||||
const name = entry && entry.icon ? entry.icon : ""
|
||||
return iconFromName(name || fallback, fallback)
|
||||
} catch (e) {
|
||||
return iconFromName(fallback, fallback)
|
||||
}
|
||||
}
|
||||
|
||||
// Distro logo helper (absolute path or empty string)
|
||||
function distroLogoPath() {
|
||||
try {
|
||||
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
152
Commons/Time.qml
@@ -8,99 +8,81 @@ import qs.Services
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Current date
|
||||
property var date: new Date()
|
||||
property string time: {
|
||||
let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm"
|
||||
let timeString = Qt.formatDateTime(date, timeFormat)
|
||||
|
||||
if (Settings.data.location.showDateWithClock) {
|
||||
let dayName = date.toLocaleDateString(Qt.locale(), "ddd")
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||
let day = date.getDate()
|
||||
let month = date.toLocaleDateString(Qt.locale(), "MMM")
|
||||
return timeString + " - " + dayName + ", " + day + " " + month
|
||||
}
|
||||
|
||||
return timeString
|
||||
}
|
||||
readonly property string dateString: {
|
||||
let now = date
|
||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||
let day = now.getDate()
|
||||
let suffix
|
||||
if (day > 3 && day < 21)
|
||||
suffix = 'th'
|
||||
else
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
suffix = "st"
|
||||
break
|
||||
case 2:
|
||||
suffix = "nd"
|
||||
break
|
||||
case 3:
|
||||
suffix = "rd"
|
||||
break
|
||||
default:
|
||||
suffix = "th"
|
||||
}
|
||||
let month = now.toLocaleDateString(Qt.locale(), "MMMM")
|
||||
let year = now.toLocaleDateString(Qt.locale(), "yyyy")
|
||||
return `${dayName}, `
|
||||
+ (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
|
||||
}
|
||||
|
||||
// Returns a Unix Timestamp (in seconds)
|
||||
readonly property int timestamp: {
|
||||
return Math.floor(date / 1000)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formats a Date object into a YYYYMMDD-HHMMSS string.
|
||||
* @param {Date} [date=new Date()] - The date to format. Defaults to the current date and time.
|
||||
* @returns {string} The formatted date string.
|
||||
*/
|
||||
function getFormattedTimestamp(date = new Date()) {
|
||||
const year = date.getFullYear()
|
||||
|
||||
// getMonth() is zero-based, so we add 1
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${year}${month}${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
// Format an easy to read approximate duration ex: 4h32m
|
||||
// Used to display the time remaining on the Battery widget
|
||||
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60)
|
||||
const seconds = totalSeconds - (hours * 3600) - (minutes * 60)
|
||||
|
||||
var str = ""
|
||||
if (hours) {
|
||||
str += hours.toString() + "h"
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.date = new Date()
|
||||
}
|
||||
if (minutes) {
|
||||
str += minutes.toString() + "m"
|
||||
}
|
||||
if (!hours && !minutes) {
|
||||
str += seconds.toString() + "s"
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
// Formats a Date object into a YYYYMMDD-HHMMSS string.
|
||||
function getFormattedTimestamp(date) {
|
||||
if (!date) {
|
||||
date = new Date()
|
||||
}
|
||||
const year = date.getFullYear()
|
||||
|
||||
onTriggered: root.date = new Date()
|
||||
}
|
||||
// getMonth() is zero-based, so we add 1
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${year}${month}${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
// Format an easy to read approximate duration ex: 4h32m
|
||||
// Used to display the time remaining on the Battery widget, computer uptime, etc..
|
||||
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
|
||||
return '0s'
|
||||
}
|
||||
|
||||
// Floor the input to handle decimal seconds
|
||||
totalSeconds = Math.floor(totalSeconds)
|
||||
|
||||
const days = Math.floor(totalSeconds / 86400)
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
|
||||
const parts = []
|
||||
if (days)
|
||||
parts.push(`${days}d`)
|
||||
if (hours)
|
||||
parts.push(`${hours}h`)
|
||||
if (minutes)
|
||||
parts.push(`${minutes}m`)
|
||||
|
||||
// Only show seconds if no hours and no minutes
|
||||
if (!hours && !minutes) {
|
||||
parts.push(`${seconds}s`)
|
||||
}
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
// Format a date into
|
||||
function formatRelativeTime(date) {
|
||||
if (!date)
|
||||
return ""
|
||||
const diff = Date.now() - date.getTime()
|
||||
if (diff < 60000)
|
||||
return "now"
|
||||
if (diff < 3600000)
|
||||
return `${Math.floor(diff / 60000)}m ago`
|
||||
if (diff < 86400000)
|
||||
return `${Math.floor(diff / 3600000)}h ago`
|
||||
return `${Math.floor(diff / 86400000)}d ago`
|
||||
}
|
||||
}
|
||||
|
||||
114
Helpers/QtObj2JS.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// -----------------------------------------------------
|
||||
// Helper function to convert Qt objects to plain JavaScript objects
|
||||
// Only used when generating settings-default.json
|
||||
function qtObjectToPlainObject(obj) {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Handle primitive types
|
||||
if (typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Handle native JavaScript arrays
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => qtObjectToPlainObject(item));
|
||||
}
|
||||
|
||||
// Detect QML arrays FIRST (before color detection)
|
||||
// QML arrays have a numeric length property and indexed properties
|
||||
if (typeof obj.length === "number" && obj.length >= 0) {
|
||||
// Check if it has indexed properties - be more flexible about detection
|
||||
var hasIndexedProps = true;
|
||||
var hasNumericKeys = false;
|
||||
|
||||
// Check if we have at least some numeric properties
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
if (obj.hasOwnProperty(i) || obj[i] !== undefined) {
|
||||
hasNumericKeys = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have length > 0 and some numeric keys, treat as array
|
||||
if (obj.length > 0 && hasNumericKeys) {
|
||||
var arr = [];
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
// Use direct property access, handle undefined gracefully
|
||||
var item = obj[i];
|
||||
if (item !== undefined) {
|
||||
arr.push(qtObjectToPlainObject(item));
|
||||
}
|
||||
}
|
||||
return arr; // Return here to avoid processing as object
|
||||
}
|
||||
|
||||
// Handle empty arrays (length = 0)
|
||||
if (obj.length === 0) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Detect and convert QML color objects to hex strings
|
||||
if (
|
||||
typeof obj.r === "number" &&
|
||||
typeof obj.g === "number" &&
|
||||
typeof obj.b === "number" &&
|
||||
typeof obj.a === "number" &&
|
||||
typeof obj.valid === "boolean"
|
||||
) {
|
||||
// This looks like a QML color object
|
||||
try {
|
||||
// Try to get the string representation (should be hex like "#000000")
|
||||
if (typeof obj.toString === "function") {
|
||||
return obj.toString();
|
||||
} else {
|
||||
// Fallback: convert RGBA to hex manually
|
||||
var r = Math.round(obj.r * 255);
|
||||
var g = Math.round(obj.g * 255);
|
||||
var b = Math.round(obj.b * 255);
|
||||
var hex =
|
||||
"#" +
|
||||
r.toString(16).padStart(2, "0") +
|
||||
g.toString(16).padStart(2, "0") +
|
||||
b.toString(16).padStart(2, "0");
|
||||
return hex;
|
||||
}
|
||||
} catch (e) {
|
||||
// If conversion fails, fall through to regular object handling
|
||||
}
|
||||
}
|
||||
|
||||
// Handle regular objects
|
||||
var plainObj = {};
|
||||
|
||||
// Get all property names, but filter out Qt-specific ones
|
||||
var propertyNames = Object.getOwnPropertyNames(obj);
|
||||
|
||||
for (var i = 0; i < propertyNames.length; i++) {
|
||||
var propName = propertyNames[i];
|
||||
|
||||
// Skip Qt-specific properties, functions, and array-like properties
|
||||
if (
|
||||
propName === "objectName" ||
|
||||
propName === "objectNameChanged" ||
|
||||
propName === "length" || // Skip length property
|
||||
/^\d+$/.test(propName) || // Skip numeric keys (0, 1, 2, etc.)
|
||||
propName.endsWith("Changed") ||
|
||||
typeof obj[propName] === "function"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
var value = obj[propName];
|
||||
plainObj[propName] = qtObjectToPlainObject(value);
|
||||
} catch (e) {
|
||||
// Skip properties that can't be accessed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return plainObj;
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NPanel {
|
||||
id: root
|
||||
panelWidth: 380 * scaling
|
||||
panelHeight: 500 * scaling
|
||||
panelAnchorRight: true
|
||||
|
||||
// Auto-refresh when service updates
|
||||
Connections {
|
||||
target: ArchUpdaterService
|
||||
function onUpdatePackagesChanged() {
|
||||
// Force UI update when packages change
|
||||
if (root.visible) {
|
||||
// Small delay to ensure data is fully updated
|
||||
Qt.callLater(() => {
|
||||
// Force a UI update by triggering a property change
|
||||
ArchUpdaterService.updatePackages = ArchUpdaterService.updatePackages
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panelContent: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
text: "system_update"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "System Updates"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
sizeMultiplier: 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Update summary
|
||||
Text {
|
||||
text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length
|
||||
!== 1 ? "s" : "") + " can be updated"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Package selection info
|
||||
Text {
|
||||
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Package list
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
ListView {
|
||||
id: packageListView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
clip: true
|
||||
model: ArchUpdaterService.updatePackages
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
delegate: Rectangle {
|
||||
width: packageListView.width
|
||||
height: 50 * scaling
|
||||
color: Color.transparent
|
||||
radius: Style.radiusS * scaling
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Checkbox for selection
|
||||
NIconButton {
|
||||
id: checkbox
|
||||
icon: "check_box_outline_blank"
|
||||
onClicked: {
|
||||
const isSelected = ArchUpdaterService.isPackageSelected(modelData.name)
|
||||
if (isSelected) {
|
||||
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||
icon = "check_box_outline_blank"
|
||||
colorFg = Color.mOnSurfaceVariant
|
||||
} else {
|
||||
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||
icon = "check_box"
|
||||
colorFg = Color.mPrimary
|
||||
}
|
||||
}
|
||||
colorBg: Color.transparent
|
||||
colorFg: Color.mOnSurfaceVariant
|
||||
Layout.preferredWidth: 30 * scaling
|
||||
Layout.preferredHeight: 30 * scaling
|
||||
|
||||
Component.onCompleted: {
|
||||
// Set initial state
|
||||
if (ArchUpdaterService.isPackageSelected(modelData.name)) {
|
||||
icon = "check_box"
|
||||
colorFg = Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Package info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.oldVersion + " → " + modelData.newVersion
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: "Check for updates"
|
||||
onClicked: {
|
||||
ArchUpdaterService.doPoll()
|
||||
}
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 35 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update"
|
||||
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
|
||||
enabled: !ArchUpdaterService.updateInProgress
|
||||
onClicked: {
|
||||
ArchUpdaterService.runUpdate()
|
||||
root.close()
|
||||
}
|
||||
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary
|
||||
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 35 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings"
|
||||
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
|
||||
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
|
||||
onClicked: {
|
||||
if (ArchUpdaterService.selectedPackagesCount > 0) {
|
||||
ArchUpdaterService.runSelectiveUpdate()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||
> 0 ? Color.mSecondary : Color.mSurfaceVariant)
|
||||
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||
> 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant)
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 35 * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,28 +3,89 @@ import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Loader {
|
||||
active: !Settings.data.wallpaper.swww.enabled
|
||||
Variants {
|
||||
id: backgroundVariants
|
||||
model: Quickshell.screens
|
||||
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: Loader {
|
||||
|
||||
delegate: PanelWindow {
|
||||
required property ShellScreen modelData
|
||||
property string wallpaperSource: WallpaperService.currentWallpaper !== ""
|
||||
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
|
||||
required property ShellScreen modelData
|
||||
|
||||
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled
|
||||
active: Settings.isLoaded && modelData && Settings.data.wallpaper.enabled
|
||||
|
||||
// Force update when SWWW setting changes
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
sourceComponent: PanelWindow {
|
||||
id: root
|
||||
|
||||
} else {
|
||||
// Internal state management
|
||||
property string transitionType: "fade"
|
||||
property real transitionProgress: 0
|
||||
|
||||
readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness
|
||||
readonly property var allTransitions: WallpaperService.allTransitions
|
||||
readonly property bool transitioning: transitionAnimation.running
|
||||
|
||||
// Wipe direction: 0=left, 1=right, 2=up, 3=down
|
||||
property real wipeDirection: 0
|
||||
|
||||
// Disc
|
||||
property real discCenterX: 0.5
|
||||
property real discCenterY: 0.5
|
||||
|
||||
// Stripe
|
||||
property real stripesCount: 16
|
||||
property real stripesAngle: 0
|
||||
|
||||
// Used to debounce wallpaper changes
|
||||
property string futureWallpaper: ""
|
||||
|
||||
// Fillmode default is "crop"
|
||||
property real fillMode: 1.0
|
||||
property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0)
|
||||
|
||||
// On startup, defer assigning wallpaper until the service cache is ready
|
||||
function _startWallpaperOnceReady() {
|
||||
if (!modelData) {
|
||||
Qt.callLater(_startWallpaperOnceReady)
|
||||
return
|
||||
}
|
||||
|
||||
var cacheReady = WallpaperService && WallpaperService.currentWallpapers && Object.keys(WallpaperService.currentWallpapers).length > 0
|
||||
if (!cacheReady) {
|
||||
// Try again on the next tick until WallpaperService.init() populates cache
|
||||
Qt.callLater(_startWallpaperOnceReady)
|
||||
return
|
||||
}
|
||||
|
||||
fillMode = WallpaperService.getFillModeUniform()
|
||||
var path = WallpaperService.getWallpaper(modelData.name)
|
||||
setWallpaperImmediate(path)
|
||||
}
|
||||
|
||||
Component.onCompleted: _startWallpaperOnceReady()
|
||||
|
||||
Connections {
|
||||
target: Settings.data.wallpaper
|
||||
function onFillModeChanged() {
|
||||
fillMode = WallpaperService.getFillModeUniform()
|
||||
}
|
||||
}
|
||||
|
||||
// External state management
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
function onWallpaperChanged(screenName, path) {
|
||||
if (screenName === modelData.name) {
|
||||
|
||||
// Update wallpaper display
|
||||
// Set wallpaper immediately on startup
|
||||
futureWallpaper = path
|
||||
debounceTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: Color.transparent
|
||||
screen: modelData
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
@@ -38,18 +99,228 @@ Loader {
|
||||
left: true
|
||||
}
|
||||
|
||||
margins {
|
||||
top: 0
|
||||
Timer {
|
||||
id: debounceTimer
|
||||
interval: 333
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
changeWallpaper()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: wallpaperSource
|
||||
visible: wallpaperSource !== ""
|
||||
cache: true
|
||||
id: currentWallpaper
|
||||
source: ""
|
||||
smooth: true
|
||||
mipmap: false
|
||||
visible: false
|
||||
cache: false
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
Image {
|
||||
id: nextWallpaper
|
||||
source: ""
|
||||
smooth: true
|
||||
mipmap: false
|
||||
visible: false
|
||||
cache: false
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
// Fade or None transition shader
|
||||
ShaderEffect {
|
||||
id: fadeShader
|
||||
anchors.fill: parent
|
||||
visible: transitionType === "fade" || transitionType === "none"
|
||||
|
||||
property variant source1: currentWallpaper
|
||||
property variant source2: nextWallpaper
|
||||
property real progress: root.transitionProgress
|
||||
|
||||
// Fill mode properties
|
||||
property real fillMode: root.fillMode
|
||||
property vector4d fillColor: root.fillColor
|
||||
property real imageWidth1: source1.sourceSize.width
|
||||
property real imageHeight1: source1.sourceSize.height
|
||||
property real imageWidth2: source2.sourceSize.width
|
||||
property real imageHeight2: source2.sourceSize.height
|
||||
property real screenWidth: width
|
||||
property real screenHeight: height
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_fade.frag.qsb")
|
||||
}
|
||||
|
||||
// Wipe transition shader
|
||||
ShaderEffect {
|
||||
id: wipeShader
|
||||
anchors.fill: parent
|
||||
visible: transitionType === "wipe"
|
||||
|
||||
property variant source1: currentWallpaper
|
||||
property variant source2: nextWallpaper
|
||||
property real progress: root.transitionProgress
|
||||
property real smoothness: root.edgeSmoothness
|
||||
property real direction: root.wipeDirection
|
||||
|
||||
// Fill mode properties
|
||||
property real fillMode: root.fillMode
|
||||
property vector4d fillColor: root.fillColor
|
||||
property real imageWidth1: source1.sourceSize.width
|
||||
property real imageHeight1: source1.sourceSize.height
|
||||
property real imageWidth2: source2.sourceSize.width
|
||||
property real imageHeight2: source2.sourceSize.height
|
||||
property real screenWidth: width
|
||||
property real screenHeight: height
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_wipe.frag.qsb")
|
||||
}
|
||||
|
||||
// Disc reveal transition shader
|
||||
ShaderEffect {
|
||||
id: discShader
|
||||
anchors.fill: parent
|
||||
visible: transitionType === "disc"
|
||||
|
||||
property variant source1: currentWallpaper
|
||||
property variant source2: nextWallpaper
|
||||
property real progress: root.transitionProgress
|
||||
property real smoothness: root.edgeSmoothness
|
||||
property real aspectRatio: root.width / root.height
|
||||
property real centerX: root.discCenterX
|
||||
property real centerY: root.discCenterY
|
||||
|
||||
// Fill mode properties
|
||||
property real fillMode: root.fillMode
|
||||
property vector4d fillColor: root.fillColor
|
||||
property real imageWidth1: source1.sourceSize.width
|
||||
property real imageHeight1: source1.sourceSize.height
|
||||
property real imageWidth2: source2.sourceSize.width
|
||||
property real imageHeight2: source2.sourceSize.height
|
||||
property real screenWidth: width
|
||||
property real screenHeight: height
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_disc.frag.qsb")
|
||||
}
|
||||
|
||||
// Diagonal stripes transition shader
|
||||
ShaderEffect {
|
||||
id: stripesShader
|
||||
anchors.fill: parent
|
||||
visible: transitionType === "stripes"
|
||||
|
||||
property variant source1: currentWallpaper
|
||||
property variant source2: nextWallpaper
|
||||
property real progress: root.transitionProgress
|
||||
property real smoothness: root.edgeSmoothness
|
||||
property real aspectRatio: root.width / root.height
|
||||
property real stripeCount: root.stripesCount
|
||||
property real angle: root.stripesAngle
|
||||
|
||||
// Fill mode properties
|
||||
property real fillMode: root.fillMode
|
||||
property vector4d fillColor: root.fillColor
|
||||
property real imageWidth1: source1.sourceSize.width
|
||||
property real imageHeight1: source1.sourceSize.height
|
||||
property real imageWidth2: source2.sourceSize.width
|
||||
property real imageHeight2: source2.sourceSize.height
|
||||
property real screenWidth: width
|
||||
property real screenHeight: height
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_stripes.frag.qsb")
|
||||
}
|
||||
|
||||
// Animation for the transition progress
|
||||
NumberAnimation {
|
||||
id: transitionAnimation
|
||||
target: root
|
||||
property: "transitionProgress"
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
// The stripes shader feels faster visually, we make it a bit slower here.
|
||||
duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration * 1.6 : Settings.data.wallpaper.transitionDuration
|
||||
easing.type: Easing.InOutCubic
|
||||
onFinished: {
|
||||
// Swap images after transition completes
|
||||
if (currentWallpaper.source !== "") {
|
||||
currentWallpaper.source = ""
|
||||
}
|
||||
currentWallpaper.source = nextWallpaper.source
|
||||
nextWallpaper.source = ""
|
||||
transitionProgress = 0.0
|
||||
Qt.callLater(() => {
|
||||
currentWallpaper.asynchronous = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setWallpaperImmediate(source) {
|
||||
transitionAnimation.stop()
|
||||
transitionProgress = 0.0
|
||||
if (currentWallpaper.source !== "") {
|
||||
currentWallpaper.source = ""
|
||||
}
|
||||
currentWallpaper.source = source
|
||||
nextWallpaper.source = ""
|
||||
}
|
||||
|
||||
function setWallpaperWithTransition(source) {
|
||||
if (source === currentWallpaper.source) {
|
||||
return
|
||||
}
|
||||
|
||||
if (transitioning) {
|
||||
// We are interrupting a transition
|
||||
transitionAnimation.stop()
|
||||
transitionProgress = 0
|
||||
currentWallpaper.source = nextWallpaper.source
|
||||
nextWallpaper.source = ""
|
||||
}
|
||||
|
||||
nextWallpaper.source = source
|
||||
currentWallpaper.asynchronous = false
|
||||
transitionAnimation.start()
|
||||
}
|
||||
|
||||
// Main method that actually trigger the wallpaper change
|
||||
function changeWallpaper() {
|
||||
// Get the transitionType from the settings
|
||||
transitionType = Settings.data.wallpaper.transitionType
|
||||
|
||||
if (transitionType == "random") {
|
||||
var index = Math.floor(Math.random() * allTransitions.length)
|
||||
transitionType = allTransitions[index]
|
||||
}
|
||||
|
||||
// Ensure the transition type really exists
|
||||
if (transitionType !== "none" && !allTransitions.includes(transitionType)) {
|
||||
transitionType = "fade"
|
||||
}
|
||||
|
||||
//Logger.log("Background", "New wallpaper: ", futureWallpaper, "On:", modelData.name, "Transition:", transitionType)
|
||||
switch (transitionType) {
|
||||
case "none":
|
||||
setWallpaperImmediate(futureWallpaper)
|
||||
break
|
||||
case "wipe":
|
||||
wipeDirection = Math.random() * 4
|
||||
setWallpaperWithTransition(futureWallpaper)
|
||||
break
|
||||
case "disc":
|
||||
discCenterX = Math.random()
|
||||
discCenterY = Math.random()
|
||||
setWallpaperWithTransition(futureWallpaper)
|
||||
break
|
||||
case "stripes":
|
||||
stripesCount = Math.round(Math.random() * 20 + 4)
|
||||
stripesAngle = Math.random() * 360
|
||||
setWallpaperWithTransition(futureWallpaper)
|
||||
break
|
||||
default:
|
||||
setWallpaperWithTransition(futureWallpaper)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,47 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Loader {
|
||||
active: CompositorService.isNiri
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Component.onCompleted: {
|
||||
if (CompositorService.isNiri) {
|
||||
Logger.log("Overview", "Loading Overview component for Niri")
|
||||
}
|
||||
}
|
||||
delegate: Loader {
|
||||
required property ShellScreen modelData
|
||||
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
active: CompositorService.isNiri && CompositorService.niriOverviewActive && modelData && Settings.data.wallpaper.enabled
|
||||
|
||||
delegate: PanelWindow {
|
||||
required property ShellScreen modelData
|
||||
property string wallpaperSource: WallpaperService.currentWallpaper !== ""
|
||||
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
|
||||
property string wallpaper: ""
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
Component.onCompleted: {
|
||||
if (modelData) {
|
||||
Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
|
||||
}
|
||||
updateWallpaper()
|
||||
}
|
||||
|
||||
function updateWallpaper() {
|
||||
wallpaper = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
|
||||
}
|
||||
|
||||
// External state management
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
function onWallpaperChanged(screenName, path) {
|
||||
if (screenName === modelData.name) {
|
||||
wallpaper = path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
function onIsInitializedChanged() {
|
||||
if (WallpaperService.isInitialized) {
|
||||
updateWallpaper()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled
|
||||
color: Color.transparent
|
||||
screen: modelData
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
@@ -39,29 +62,27 @@ Loader {
|
||||
|
||||
Image {
|
||||
id: bgImage
|
||||
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: wallpaperSource
|
||||
cache: true
|
||||
source: wallpaper
|
||||
smooth: true
|
||||
mipmap: false
|
||||
visible: wallpaperSource !== ""
|
||||
cache: false
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
id: overviewBgBlur
|
||||
|
||||
anchors.fill: parent
|
||||
source: bgImage
|
||||
autoPaddingEnabled: false
|
||||
blurEnabled: true
|
||||
blur: 0.48
|
||||
blurMax: 128
|
||||
}
|
||||
|
||||
// Make the overview darker
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, 0.5)
|
||||
color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface, Style.opacityMedium) : Qt.alpha(Color.mOnSurface, Style.opacityMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,23 +16,26 @@ Loader {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: ScalingService.getScreenScale(screen)
|
||||
screen: modelData
|
||||
|
||||
// Visible color
|
||||
property color ringColor: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b,
|
||||
Settings.data.bar.backgroundOpacity)
|
||||
// The amount subtracted from full size for the inner cutout
|
||||
// Inner size = full size - borderWidth (per axis)
|
||||
property int borderWidth: Style.borderM
|
||||
// Rounded radius for the inner cutout
|
||||
property int innerRadius: 20
|
||||
property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Qt.rgba(0, 0, 0, 1) : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
|
||||
property real cornerRadius: Style.screenRadius * scaling
|
||||
property real cornerSize: Style.screenRadius * scaling
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if (screenName === screen.name) {
|
||||
scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: Color.transparent
|
||||
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "quickshell-corner"
|
||||
// Do not take keyboard focus and make the surface click-through
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
@@ -43,111 +46,205 @@ Loader {
|
||||
}
|
||||
|
||||
margins {
|
||||
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|
||||
|| (Settings.data.bar.monitors.length === 0))
|
||||
&& Settings.data.bar.position === "top" ? Math.floor(Style.barHeight * scaling) : 0
|
||||
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|
||||
|| (Settings.data.bar.monitors.length === 0))
|
||||
&& Settings.data.bar.position === "bottom" ? Math.floor(Style.barHeight * scaling) : 0
|
||||
}
|
||||
|
||||
// Source we want to show only as a ring
|
||||
Rectangle {
|
||||
id: overlaySource
|
||||
|
||||
anchors.fill: parent
|
||||
color: root.ringColor
|
||||
}
|
||||
|
||||
// Texture for overlaySource
|
||||
ShaderEffectSource {
|
||||
id: overlayTexture
|
||||
|
||||
anchors.fill: parent
|
||||
sourceItem: overlaySource
|
||||
hideSource: true
|
||||
live: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
// Mask via Canvas: paint opaque white, then punch rounded inner hole
|
||||
Canvas {
|
||||
id: maskSource
|
||||
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
// Solid white base (alpha=1)
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
ctx.fillStyle = "#ffffffff"
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
// Punch hole using destination-out with rounded rect path
|
||||
const x = Math.round(root.borderWidth / 2)
|
||||
const y = Math.round(root.borderWidth / 2)
|
||||
const w = Math.max(0, width - root.borderWidth)
|
||||
const h = Math.max(0, height - root.borderWidth)
|
||||
const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2))
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffffff"
|
||||
ctx.beginPath()
|
||||
// rounded rectangle path using arcTo
|
||||
ctx.moveTo(x + r, y)
|
||||
ctx.lineTo(x + w - r, y)
|
||||
ctx.arcTo(x + w, y, x + w, y + r, r)
|
||||
ctx.lineTo(x + w, y + h - r)
|
||||
ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
|
||||
ctx.lineTo(x + r, y + h)
|
||||
ctx.arcTo(x, y + h, x, y + h - r, r)
|
||||
ctx.lineTo(x, y + r)
|
||||
ctx.arcTo(x, y, x + r, y, r)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
}
|
||||
|
||||
// Repaint mask when properties change
|
||||
Connections {
|
||||
function onBorderWidthChanged() {
|
||||
maskSource.requestPaint()
|
||||
}
|
||||
|
||||
function onRingColorChanged() {}
|
||||
|
||||
function onInnerRadiusChanged() {
|
||||
maskSource.requestPaint()
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
// Texture for maskSource; hides the original
|
||||
ShaderEffectSource {
|
||||
id: maskTexture
|
||||
|
||||
anchors.fill: parent
|
||||
sourceItem: maskSource
|
||||
hideSource: true
|
||||
live: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
// Apply mask to show only the ring area
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: overlayTexture
|
||||
maskEnabled: true
|
||||
maskSource: maskTexture
|
||||
maskInverted: false
|
||||
maskSpreadAtMax: 0.75
|
||||
// When bar is floating, corners should be at screen edges (no margins)
|
||||
// When bar is not floating, respect bar margins as before
|
||||
top: !Settings.data.bar.floating && BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||
bottom: !Settings.data.bar.floating && BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||
left: !Settings.data.bar.floating && BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||
right: !Settings.data.bar.floating && BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||
}
|
||||
|
||||
mask: Region {}
|
||||
|
||||
// Top-left concave corner
|
||||
Canvas {
|
||||
id: topLeftCorner
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: cornerSize
|
||||
height: cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: false
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Fill the entire area with the corner color
|
||||
ctx.fillStyle = root.cornerColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// Cut out the rounded corner using destination-out
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(width, height, root.cornerRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCornerColorChanged() {
|
||||
if (topLeftCorner.available)
|
||||
topLeftCorner.requestPaint()
|
||||
}
|
||||
function onCornerRadiusChanged() {
|
||||
if (topLeftCorner.available)
|
||||
topLeftCorner.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Top-right concave corner
|
||||
Canvas {
|
||||
id: topRightCorner
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: cornerSize
|
||||
height: cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
ctx.fillStyle = root.cornerColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, height, root.cornerRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCornerColorChanged() {
|
||||
if (topRightCorner.available)
|
||||
topRightCorner.requestPaint()
|
||||
}
|
||||
function onCornerRadiusChanged() {
|
||||
if (topRightCorner.available)
|
||||
topRightCorner.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom-left concave corner
|
||||
Canvas {
|
||||
id: bottomLeftCorner
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
width: cornerSize
|
||||
height: cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
ctx.fillStyle = root.cornerColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(width, 0, root.cornerRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCornerColorChanged() {
|
||||
if (bottomLeftCorner.available)
|
||||
bottomLeftCorner.requestPaint()
|
||||
}
|
||||
function onCornerRadiusChanged() {
|
||||
if (bottomLeftCorner.available)
|
||||
bottomLeftCorner.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom-right concave corner
|
||||
Canvas {
|
||||
id: bottomRightCorner
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: cornerSize
|
||||
height: cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
ctx.fillStyle = root.cornerColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, root.cornerRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCornerColorChanged() {
|
||||
if (bottomRightCorner.available)
|
||||
bottomRightCorner.requestPaint()
|
||||
}
|
||||
function onCornerRadiusChanged() {
|
||||
if (bottomRightCorner.available)
|
||||
bottomRightCorner.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,119 +8,251 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Notification
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: PanelWindow {
|
||||
delegate: Loader {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property real scaling: ScalingService.scale(screen)
|
||||
screen: modelData
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
WlrLayershell.namespace: "noctalia-bar"
|
||||
|
||||
implicitHeight: Style.barHeight * scaling
|
||||
color: Color.transparent
|
||||
|
||||
// If no bar activated in settings, then show them all
|
||||
visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name)
|
||||
|| (Settings.data.bar.monitors.length === 0)) : false
|
||||
|
||||
anchors {
|
||||
top: Settings.data.bar.position === "top"
|
||||
bottom: Settings.data.bar.position === "bottom"
|
||||
left: true
|
||||
right: true
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if ((modelData !== null) && (screenName === modelData.name)) {
|
||||
scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
active: BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
|
||||
// Background fill
|
||||
Rectangle {
|
||||
id: bar
|
||||
sourceComponent: PanelWindow {
|
||||
screen: modelData || null
|
||||
|
||||
WlrLayershell.namespace: "noctalia-bar"
|
||||
|
||||
implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling)
|
||||
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width
|
||||
color: Color.transparent
|
||||
|
||||
anchors {
|
||||
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||
right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||
}
|
||||
|
||||
// Floating bar margins - only apply when floating is enabled
|
||||
// Also don't apply margin on the opposite side ot the bar orientation, ex: if bar is floating on top, margin is only applied on top, not bottom.
|
||||
margins {
|
||||
top: Settings.data.bar.floating && Settings.data.bar.position !== "bottom" ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
bottom: Settings.data.bar.floating && Settings.data.bar.position !== "top" ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
left: Settings.data.bar.floating && Settings.data.bar.position !== "right" ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
right: Settings.data.bar.floating && Settings.data.bar.position !== "left" ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (modelData && modelData.name) {
|
||||
BarService.registerBar(modelData.name)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, Settings.data.bar.backgroundOpacity)
|
||||
layer.enabled: true
|
||||
}
|
||||
clip: true
|
||||
|
||||
// ------------------------------
|
||||
// Left Section - Dynamic Widgets
|
||||
Row {
|
||||
id: leftSection
|
||||
// Background fill with shadow
|
||||
Rectangle {
|
||||
id: bar
|
||||
|
||||
height: parent.height
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
anchors.fill: parent
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: Loader {
|
||||
active: true
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
}
|
||||
// Floating bar rounded corners
|
||||
radius: Settings.data.bar.floating ? Style.radiusL : 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter"))
|
||||
mouse.accepted = true
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Center Section - Dynamic Widgets
|
||||
Row {
|
||||
id: centerSection
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent
|
||||
}
|
||||
|
||||
height: parent.height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
// For vertical bars
|
||||
Component {
|
||||
id: verticalBarComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: Loader {
|
||||
active: true
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
// Top section (left widgets)
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.marginM * root.scaling
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "left",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center section (center widgets)
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "center",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom section (right widgets)
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Style.marginM * root.scaling
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "right",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Right Section - Dynamic Widgets
|
||||
Row {
|
||||
id: rightSection
|
||||
// For horizontal bars
|
||||
Component {
|
||||
id: horizontalBarComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
height: parent.height
|
||||
anchors.right: bar.right
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: bar.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
// Left Section
|
||||
RowLayout {
|
||||
id: leftSection
|
||||
objectName: "leftSection"
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * root.scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: Loader {
|
||||
active: true
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "left",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center Section
|
||||
RowLayout {
|
||||
id: centerSection
|
||||
objectName: "centerSection"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "center",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right Section
|
||||
RowLayout {
|
||||
id: rightSection
|
||||
objectName: "rightSection"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.marginS * root.scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "right",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
181
Modules/Bar/Bluetooth/BluetoothDevicesList.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string tooltipText: ""
|
||||
property var model: {
|
||||
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: root.label
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mSecondary
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.fillWidth: true
|
||||
visible: root.model.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: deviceList
|
||||
Layout.fillWidth: true
|
||||
model: root.model
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Rectangle {
|
||||
id: device
|
||||
|
||||
readonly property bool canConnect: BluetoothService.canConnect(modelData)
|
||||
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
|
||||
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||
|
||||
function getContentColor(defaultColor = Color.mOnSurface) {
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Color.mPrimary
|
||||
if (modelData.blocked)
|
||||
return Color.mError
|
||||
return defaultColor
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginM * scaling * 2)
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: getContentColor(Color.mOutline)
|
||||
|
||||
RowLayout {
|
||||
id: deviceLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// One device BT icon
|
||||
NIcon {
|
||||
icon: BluetoothService.getDeviceIcon(modelData)
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
// Device name
|
||||
NText {
|
||||
text: modelData.name || modelData.deviceName
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Status
|
||||
NText {
|
||||
text: BluetoothService.getStatusString(modelData)
|
||||
visible: text !== ""
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
|
||||
// Signal Strength
|
||||
RowLayout {
|
||||
visible: modelData.signalStrength !== undefined
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
// Device signal strength - "Unknown" when not connected
|
||||
NText {
|
||||
text: BluetoothService.getSignalStrength(modelData)
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
text: BluetoothService.getSignalIcon(modelData)
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
// Battery
|
||||
NText {
|
||||
visible: modelData.batteryAvailable
|
||||
text: BluetoothService.getBattery(modelData)
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to push connect button to the right
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Call to action
|
||||
NButton {
|
||||
id: button
|
||||
visible: (modelData.state !== BluetoothDeviceState.Connecting)
|
||||
enabled: (canConnect || canDisconnect) && !isBusy
|
||||
outlined: !button.hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
fontWeight: Style.fontWeightMedium
|
||||
backgroundColor: {
|
||||
if (device.canDisconnect && !isBusy) {
|
||||
return Color.mError
|
||||
}
|
||||
return Color.mPrimary
|
||||
}
|
||||
tooltipText: root.tooltipText
|
||||
text: {
|
||||
if (modelData.pairing) {
|
||||
return "Pairing..."
|
||||
}
|
||||
if (modelData.blocked) {
|
||||
return "Blocked"
|
||||
}
|
||||
if (modelData.connected) {
|
||||
return "Disconnect"
|
||||
}
|
||||
return "Connect"
|
||||
}
|
||||
icon: (isBusy ? "busy" : null)
|
||||
onClicked: {
|
||||
if (modelData.connected) {
|
||||
BluetoothService.disconnectDevice(modelData)
|
||||
} else {
|
||||
BluetoothService.connectDeviceWithTrust(modelData)
|
||||
}
|
||||
}
|
||||
onRightClicked: {
|
||||
BluetoothService.forgetDevice(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
223
Modules/Bar/Bluetooth/BluetoothPanel.qml
Normal file
@@ -0,0 +1,223 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: 380
|
||||
preferredHeight: 500
|
||||
panelKeyboardFocus: true
|
||||
|
||||
panelContent: Rectangle {
|
||||
color: Color.transparent
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// HEADER
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "bluetooth"
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.title")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: bluetoothSwitch
|
||||
checked: BluetoothService.enabled
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.65 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
enabled: BluetoothService.enabled
|
||||
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
|
||||
tooltipText: I18n.tr("tooltips.refresh-devices")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.transparent
|
||||
|
||||
// Center the content within this rectangle
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "bluetooth-off"
|
||||
pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.disabled")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.enable-message")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
clip: true
|
||||
contentWidth: availableWidth
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Connected devices
|
||||
BluetoothDevicesList {
|
||||
label: I18n.tr("bluetooth.panel.connected-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected)
|
||||
return BluetoothService.sortDevices(filtered)
|
||||
}
|
||||
model: items
|
||||
visible: items.length > 0
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Known devices
|
||||
BluetoothDevicesList {
|
||||
label: I18n.tr("bluetooth.panel.known-devices")
|
||||
tooltipText: I18n.tr("tooltips.connect-disconnect-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted))
|
||||
return BluetoothService.sortDevices(filtered)
|
||||
}
|
||||
model: items
|
||||
visible: items.length > 0
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Available devices
|
||||
BluetoothDevicesList {
|
||||
label: I18n.tr("bluetooth.panel.available-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted)
|
||||
return BluetoothService.sortDevices(filtered)
|
||||
}
|
||||
model: items
|
||||
visible: items.length > 0
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Fallback - No devices, scanning
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginM * scaling
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
|
||||
return false
|
||||
}
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter(dev => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0)
|
||||
}).length
|
||||
return (availableCount === 0)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "refresh"
|
||||
pointSize: Style.fontSizeXXL * 1.5 * scaling
|
||||
color: Color.mPrimary
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: Style.animationSlow * 4
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.scanning")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.pairing-mode")
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
270
Modules/Bar/Calendar/CalendarPanel.qml
Normal file
@@ -0,0 +1,270 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 320 : 300
|
||||
preferredHeight: 300
|
||||
|
||||
// Main Column
|
||||
panelContent: ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
readonly property int firstDayOfWeek: Qt.locale().firstDayOfWeek
|
||||
|
||||
// Header: Month/Year with navigation
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginM * scaling
|
||||
Layout.rightMargin: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron-left"
|
||||
tooltipText: I18n.tr("tooltips.previous-month")
|
||||
onClicked: {
|
||||
let newDate = new Date(grid.year, grid.month - 1, 1)
|
||||
grid.year = newDate.getFullYear()
|
||||
grid.month = newDate.getMonth()
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: grid.title
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron-right"
|
||||
tooltipText: I18n.tr("tooltips.next-month")
|
||||
onClicked: {
|
||||
let newDate = new Date(grid.year, grid.month + 1, 1)
|
||||
grid.year = newDate.getFullYear()
|
||||
grid.month = newDate.getMonth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Divider between header and weekdays
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS * scaling
|
||||
Layout.bottomMargin: Style.marginL * scaling
|
||||
}
|
||||
|
||||
// Columns label (respects locale's first day of week)
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS * scaling // Align with grid
|
||||
Layout.rightMargin: Style.marginS * scaling
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
spacing: 0
|
||||
|
||||
// Week header spacer or label (same width as week number column)
|
||||
Item {
|
||||
visible: Settings.data.location.showWeekNumberInCalendar
|
||||
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("calendar.panel.week")
|
||||
color: Color.mOutline
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightRegular
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Day name headers - now properly aligned with calendar grid
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
columns: 7
|
||||
rows: 1
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
|
||||
Repeater {
|
||||
model: 7
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: Style.baseWidgetSize * scaling
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
let dayIndex = (content.firstDayOfWeek + index) % 7
|
||||
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
|
||||
}
|
||||
color: Color.mSecondary
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grids: days with optional week numbers
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
Layout.rightMargin: Style.marginS * scaling
|
||||
spacing: 0
|
||||
|
||||
// Week numbers column (only visible when enabled)
|
||||
ColumnLayout {
|
||||
visible: Settings.data.location.showWeekNumberInCalendar
|
||||
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: 6 // Maximum 6 weeks in a month view
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredHeight: Style.baseWidgetSize * scaling
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
color: Color.mOutline
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
text: {
|
||||
// Calculate the date shown in the first column of this row
|
||||
// MonthGrid always shows 42 days (6 weeks × 7 days)
|
||||
|
||||
// First, find the first day of the month
|
||||
let firstOfMonth = new Date(grid.year, grid.month, 1)
|
||||
|
||||
// Calculate how many days before the 1st to start the grid
|
||||
// This depends on the locale's first day of week
|
||||
let firstDayOfWeek = content.firstDayOfWeek
|
||||
let firstOfMonthDayOfWeek = firstOfMonth.getDay()
|
||||
|
||||
// Calculate offset: how many days before the 1st should the grid start?
|
||||
let daysBeforeFirst = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7
|
||||
|
||||
// MonthGrid typically shows the previous month's days to fill the first week
|
||||
// If the 1st is already on the first day of week, show the previous week
|
||||
if (daysBeforeFirst === 0) {
|
||||
daysBeforeFirst = 7
|
||||
}
|
||||
|
||||
// Calculate the start date of the grid
|
||||
let gridStartDate = new Date(grid.year, grid.month, 1 - daysBeforeFirst)
|
||||
|
||||
// Calculate the date for this specific row (week)
|
||||
let rowStartDate = new Date(gridStartDate)
|
||||
rowStartDate.setDate(gridStartDate.getDate() + (index * 7))
|
||||
|
||||
// For ISO week numbers, we need to find the Thursday of this week
|
||||
// ISO 8601 week numbering: week with year's first Thursday is week 1
|
||||
// The week number is determined by the Thursday
|
||||
|
||||
// Find the Thursday of this row's week
|
||||
// If firstDayOfWeek is Monday (1), Thursday is +3 days
|
||||
// If firstDayOfWeek is Sunday (0), we need to adjust
|
||||
let thursday = new Date(rowStartDate)
|
||||
if (firstDayOfWeek === 0) {
|
||||
// Sunday start: Thursday is 4 days after Sunday
|
||||
thursday.setDate(rowStartDate.getDate() + 4)
|
||||
} else if (firstDayOfWeek === 1) {
|
||||
// Monday start: Thursday is 3 days after Monday
|
||||
thursday.setDate(rowStartDate.getDate() + 3)
|
||||
} else {
|
||||
// Other start days: calculate offset to Thursday
|
||||
let daysToThursday = (4 - firstDayOfWeek + 7) % 7
|
||||
thursday.setDate(rowStartDate.getDate() + daysToThursday)
|
||||
}
|
||||
|
||||
return `${getISOWeekNumber(thursday)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The actual calendar grid
|
||||
MonthGrid {
|
||||
id: grid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
month: Time.date.getMonth()
|
||||
year: Time.date.getFullYear()
|
||||
locale: Qt.locale()
|
||||
|
||||
delegate: Item {
|
||||
|
||||
Rectangle {
|
||||
width: Style.baseWidgetSize * scaling
|
||||
height: Style.baseWidgetSize * scaling
|
||||
radius: width / 2
|
||||
color: model.today ? Color.mPrimary : Color.transparent
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: model.day
|
||||
color: model.today ? Color.mOnPrimary : Color.mOnSurface
|
||||
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ISO 8601 week number calculation
|
||||
// This is locale-independent and always uses Monday as first day of week
|
||||
function getISOWeekNumber(date) {
|
||||
// Create a copy and set to nearest Thursday (current date + 4 - current day number)
|
||||
// ISO week starts on Monday (1) to Sunday (7)
|
||||
const target = new Date(date.getTime())
|
||||
target.setHours(0, 0, 0, 0)
|
||||
|
||||
// Get day of week where Monday = 1, Sunday = 7
|
||||
const dayOfWeek = target.getDay() || 7
|
||||
|
||||
// Set to nearest Thursday (which determines the week number)
|
||||
target.setDate(target.getDate() + 4 - dayOfWeek)
|
||||
|
||||
// Get first day of year
|
||||
const yearStart = new Date(target.getFullYear(), 0, 1)
|
||||
|
||||
// Calculate full weeks between yearStart and target
|
||||
// Add 1 because we're counting weeks, not week differences
|
||||
const weekNumber = Math.ceil(((target - yearStart) / 86400000 + 1) / 7)
|
||||
|
||||
return weekNumber
|
||||
}
|
||||
}
|
||||
116
Modules/Bar/Extras/BarPill.qml
Normal file
@@ -0,0 +1,116 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
property bool compact: false
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Dynamic sizing based on loaded component
|
||||
width: pillLoader.item ? pillLoader.item.width : 0
|
||||
height: pillLoader.item ? pillLoader.item.height : 0
|
||||
|
||||
// Loader to switch between vertical and horizontal pill implementations
|
||||
Loader {
|
||||
id: pillLoader
|
||||
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
|
||||
|
||||
Component {
|
||||
id: verticalPillComponent
|
||||
BarPillVertical {
|
||||
screen: root.screen
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
disableOpen: root.disableOpen
|
||||
rightOpen: root.rightOpen
|
||||
hovered: root.hovered
|
||||
compact: root.compact
|
||||
onShown: root.shown()
|
||||
onHidden: root.hidden()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheel(delta)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: horizontalPillComponent
|
||||
BarPillHorizontal {
|
||||
screen: root.screen
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
disableOpen: root.disableOpen
|
||||
rightOpen: root.rightOpen
|
||||
hovered: root.hovered
|
||||
compact: root.compact
|
||||
onShown: root.shown()
|
||||
onHidden: root.hidden()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheel(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (pillLoader.item && pillLoader.item.show) {
|
||||
pillLoader.item.show()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (pillLoader.item && pillLoader.item.hide) {
|
||||
pillLoader.item.hide()
|
||||
}
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (pillLoader.item && pillLoader.item.showDelayed) {
|
||||
pillLoader.item.showDelayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,100 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property color pillColor: Color.mSurfaceVariant
|
||||
property color textColor: Color.mOnSurface
|
||||
property color iconCircleColor: Color.mPrimary
|
||||
property color iconTextColor: Color.mSurface
|
||||
property color collapsedIconColor: Color.mOnSurface
|
||||
property real sizeMultiplier: 0.8
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
property bool compact: false
|
||||
|
||||
// Effective shown state (true if hovered/animated open or forced)
|
||||
readonly property bool effectiveShown: forceOpen || showPill
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
// Exposed width logic
|
||||
readonly property int pillHeight: Style.baseWidgetSize * sizeMultiplier * scaling
|
||||
readonly property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling
|
||||
readonly property int pillPaddingHorizontal: Style.marginM * scaling
|
||||
readonly property int pillOverlap: iconSize * 0.5
|
||||
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
|
||||
readonly property int pillHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
readonly property int pillPaddingHorizontal: Math.round(Style.capsuleHeight * 0.2 * scaling)
|
||||
readonly property int pillOverlap: Math.round(Style.capsuleHeight * 0.5 * scaling)
|
||||
readonly property int pillMaxWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
|
||||
|
||||
width: iconSize + (effectiveShown ? maxPillWidth - pillOverlap : 0)
|
||||
readonly property real iconSize: Math.max(1, compact ? pillHeight * 0.65 : pillHeight * 0.48)
|
||||
readonly property real textSize: Math.max(1, compact ? pillHeight * 0.45 : pillHeight * 0.33)
|
||||
|
||||
width: pillHeight + Math.max(0, pill.width - pillOverlap)
|
||||
height: pillHeight
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onTooltipTextChanged() {
|
||||
TooltipService.updateText(root.tooltipText)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: effectiveShown ? maxPillWidth : 1
|
||||
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
width: revealed ? pillMaxWidth : 1
|
||||
height: pillHeight
|
||||
x: (iconCircle.x + iconCircle.width / 2) - width
|
||||
opacity: effectiveShown ? Style.opacityFull : Style.opacityNone
|
||||
color: pillColor
|
||||
topLeftRadius: pillHeight * 0.5
|
||||
bottomLeftRadius: pillHeight * 0.5
|
||||
|
||||
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
|
||||
(iconCircle.x + iconCircle.width / 2) - width // Opens left
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
topLeftRadius: rightOpen ? 0 : pillHeight * 0.5
|
||||
bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5
|
||||
topRightRadius: rightOpen ? pillHeight * 0.5 : 0
|
||||
bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: {
|
||||
// Better text horizontal centering
|
||||
var centerX = (parent.width - width) / 2
|
||||
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
|
||||
if (forceOpen) {
|
||||
// If its force open, the icon disc background is the same color as the bg pill move text slightly
|
||||
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
|
||||
}
|
||||
return centerX + offset
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightBold
|
||||
color: textColor
|
||||
visible: effectiveShown
|
||||
color: forceOpen ? Color.mOnSurface : Color.mPrimary
|
||||
visible: revealed
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
@@ -82,13 +115,13 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
width: pillHeight
|
||||
height: pillHeight
|
||||
radius: width * 0.5
|
||||
// When forced shown, match pill background; otherwise use accent when hovered
|
||||
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
|
||||
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
x: rightOpen ? 0 : (parent.width - width)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
@@ -98,11 +131,13 @@ Item {
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: root.icon
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
// When forced shown, use pill text color; otherwise accent color when hovered
|
||||
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
|
||||
anchors.centerIn: parent
|
||||
icon: root.icon
|
||||
pointSize: iconSize
|
||||
color: hovered ? Color.mOnTertiary : Color.mOnSurface
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +148,7 @@ Item {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: maxPillWidth
|
||||
to: pillMaxWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
@@ -153,7 +188,7 @@ Item {
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: maxPillWidth
|
||||
from: pillMaxWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
@@ -173,14 +208,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
target: pill
|
||||
delay: Style.tooltipDelayLong
|
||||
text: root.tooltipText
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
@@ -194,10 +221,12 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onEntered: {
|
||||
hovered = true
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
if (disableOpen) {
|
||||
TooltipService.show(pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
|
||||
if (disableOpen || forceClose) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
@@ -205,18 +234,23 @@ Item {
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen) {
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
TooltipService.hide()
|
||||
}
|
||||
onClicked: {
|
||||
root.clicked()
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked()
|
||||
}
|
||||
}
|
||||
onWheel: wheel => {
|
||||
root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
|
||||
function show() {
|
||||
337
Modules/Bar/Extras/BarPillVertical.qml
Normal file
@@ -0,0 +1,337 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
property bool compact: false
|
||||
|
||||
// Bar position detection for pill direction
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
// Determine pill direction based on section position
|
||||
readonly property bool openDownward: rightOpen
|
||||
readonly property bool openUpward: !rightOpen
|
||||
|
||||
// Effective shown state (true if animated open or forced, but not if force closed)
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
// Sizing logic for vertical bars
|
||||
readonly property int buttonSize: Math.round(Style.capsuleHeight * scaling)
|
||||
readonly property int pillHeight: buttonSize
|
||||
readonly property int pillPaddingVertical: 3 * 2 * scaling // Very precise adjustment don't replace by Style.margin
|
||||
readonly property int pillOverlap: buttonSize * 0.5
|
||||
readonly property int maxPillWidth: buttonSize
|
||||
readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 4)
|
||||
|
||||
readonly property real iconSize: Math.max(1, compact ? pillHeight * 0.65 : pillHeight * 0.48)
|
||||
readonly property real textSize: Math.max(1, compact ? pillHeight * 0.38 : pillHeight * 0.33)
|
||||
|
||||
// For vertical bars: width is just icon size, height includes pill space
|
||||
width: buttonSize
|
||||
height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onTooltipTextChanged() {
|
||||
TooltipService.updateText(root.tooltipText)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
width: revealed ? maxPillWidth : 1
|
||||
height: revealed ? maxPillHeight : 1
|
||||
|
||||
// Position based on direction - center the pill relative to the icon
|
||||
x: 0
|
||||
y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2)
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
// Radius logic for vertical expansion - rounded on the side that connects to icon
|
||||
topLeftRadius: openUpward ? buttonSize * 0.5 : 0
|
||||
bottomLeftRadius: openDownward ? buttonSize * 0.5 : 0
|
||||
topRightRadius: openUpward ? buttonSize * 0.5 : 0
|
||||
bottomRightRadius: openDownward ? buttonSize * 0.5 : 0
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: {
|
||||
var offset = openDownward ? pillPaddingVertical * 0.75 : -pillPaddingVertical * 0.75
|
||||
if (forceOpen) {
|
||||
// If its force open, the icon disc background is the same color as the bg pill move text slightly
|
||||
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
|
||||
}
|
||||
return offset
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: forceOpen ? Color.mOnSurface : Color.mPrimary
|
||||
visible: revealed
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: buttonSize
|
||||
height: buttonSize
|
||||
radius: width * 0.5
|
||||
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
// Icon positioning based on direction
|
||||
x: 0
|
||||
y: openUpward ? (parent.height - height) : 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
pointSize: iconSize
|
||||
color: hovered ? Color.mOnTertiary : Color.mOnSurface
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: showAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: maxPillWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: 1
|
||||
to: maxPillHeight
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onStarted: {
|
||||
showPill = true
|
||||
}
|
||||
onStopped: {
|
||||
delayedHideAnim.start()
|
||||
root.shown()
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: delayedHideAnim
|
||||
running: false
|
||||
PauseAnimation {
|
||||
duration: 2500
|
||||
}
|
||||
ScriptAction {
|
||||
script: if (shouldAnimateHide) {
|
||||
hideAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: maxPillWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: maxPillHeight
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onStopped: {
|
||||
showPill = false
|
||||
shouldAnimateHide = false
|
||||
root.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
onTriggered: {
|
||||
if (!showPill) {
|
||||
showAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onEntered: {
|
||||
hovered = true
|
||||
root.entered()
|
||||
TooltipService.show(pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
|
||||
if (disableOpen || forceClose) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide()
|
||||
}
|
||||
TooltipService.hide()
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked()
|
||||
}
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide
|
||||
showAnim.start()
|
||||
} else {
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (forceOpen) {
|
||||
return
|
||||
}
|
||||
if (showPill) {
|
||||
hideAnim.start()
|
||||
}
|
||||
showTimer.stop()
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide
|
||||
showTimer.start()
|
||||
} else {
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.stop()
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.stop()
|
||||
showPill = true
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Modules/Bar/Extras/BarWidgetLoader.qml
Normal file
@@ -0,0 +1,76 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string widgetId: ""
|
||||
property var widgetProps: ({})
|
||||
property string screenName: widgetProps.screen ? widgetProps.screen.name : ""
|
||||
property string section: widgetProps.section || ""
|
||||
property int sectionIndex: widgetProps.sectionWidgetIndex || 0
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(aScreenName, scale) {
|
||||
if (loader.item && loader.item.screen && aScreenName === screenName) {
|
||||
loader.item['scaling'] = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't reserve space unless the loaded widget is really visible
|
||||
implicitWidth: loader.item ? loader.item.visible ? loader.item.implicitWidth : 0 : 0
|
||||
implicitHeight: loader.item ? loader.item.visible ? loader.item.implicitHeight : 0 : 0
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
anchors.fill: parent
|
||||
active: widgetId !== ""
|
||||
sourceComponent: {
|
||||
if (!active) {
|
||||
return null
|
||||
}
|
||||
return BarWidgetRegistry.getWidget(widgetId)
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item && widgetProps) {
|
||||
// Apply properties to loaded widget
|
||||
for (var prop in widgetProps) {
|
||||
if (item.hasOwnProperty(prop)) {
|
||||
item[prop] = widgetProps[prop]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register this widget instance with BarService
|
||||
if (screenName && section) {
|
||||
BarService.registerWidget(screenName, section, widgetId, sectionIndex, item)
|
||||
}
|
||||
|
||||
if (item.hasOwnProperty("onLoaded")) {
|
||||
item.onLoaded()
|
||||
}
|
||||
|
||||
//Logger.log("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
// Unregister when destroyed
|
||||
if (screenName && section) {
|
||||
BarService.unregisterWidget(screenName, section, widgetId, sectionIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling
|
||||
onWidgetIdChanged: {
|
||||
if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) {
|
||||
Logger.warn("BarWidgetLoader", "Widget not found in bar registry:", widgetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,24 @@ PopupWindow {
|
||||
property real anchorY
|
||||
property bool isSubMenu: false
|
||||
property bool isHovered: rootMouseArea.containsMouse
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.getScreenScale(screen)
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if ((screen != null) && (screenName === screen.name)) {
|
||||
scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int menuWidth: 180
|
||||
|
||||
implicitWidth: menuWidth * scaling
|
||||
|
||||
// Use the content height of the Flickable for implicit height
|
||||
implicitHeight: Math.min(Screen.height * 0.9, flickable.contentHeight + (Style.marginM * 2 * scaling))
|
||||
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2 * scaling))
|
||||
visible: false
|
||||
color: Color.transparent
|
||||
anchor.item: anchorItem
|
||||
@@ -147,10 +158,9 @@ PopupWindow {
|
||||
NText {
|
||||
id: text
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled
|
||||
?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
@@ -164,11 +174,11 @@ PopupWindow {
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: modelData?.hasChildren ? "menu" : ""
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
icon: modelData?.hasChildren ? "menu" : ""
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: Color.mOnSurface
|
||||
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,9 +220,32 @@ PopupWindow {
|
||||
const submenuWidth = menuWidth * scaling // Assuming a similar width as the parent
|
||||
const overlap = 4 * scaling // A small overlap to bridge the mouse path
|
||||
|
||||
// Check if there's enough space on the right
|
||||
// Determine submenu opening direction based on bar position and available space
|
||||
let openLeft = false
|
||||
|
||||
// Check bar position first
|
||||
const barPosition = Settings.data.bar.position
|
||||
const globalPos = entry.mapToGlobal(0, 0)
|
||||
const openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width)
|
||||
|
||||
if (barPosition === "right") {
|
||||
// Bar is on the right, prefer opening submenus to the left
|
||||
openLeft = true
|
||||
} else if (barPosition === "left") {
|
||||
// Bar is on the left, prefer opening submenus to the right
|
||||
openLeft = false
|
||||
} else {
|
||||
// Bar is horizontal (top/bottom) or undefined, use space-based logic
|
||||
openLeft = (globalPos.x + entry.width + submenuWidth > screen.width)
|
||||
|
||||
// Secondary check: ensure we don't open off-screen
|
||||
if (openLeft && globalPos.x - submenuWidth < 0) {
|
||||
// Would open off the left edge, force right opening
|
||||
openLeft = false
|
||||
} else if (!openLeft && globalPos.x + entry.width + submenuWidth > screen.width) {
|
||||
// Would open off the right edge, force left opening
|
||||
openLeft = true
|
||||
}
|
||||
}
|
||||
|
||||
// Position with overlap
|
||||
const anchorX = openLeft ? -submenuWidth + overlap : entry.width - overlap
|
||||
@@ -223,7 +256,8 @@ PopupWindow {
|
||||
"anchorItem": entry,
|
||||
"anchorX": anchorX,
|
||||
"anchorY": 0,
|
||||
"isSubMenu": true
|
||||
"isSubMenu": true,
|
||||
"screen": screen
|
||||
})
|
||||
|
||||
if (entry.subMenu) {
|
||||
|
||||
587
Modules/Bar/WiFi/WiFiPanel.qml
Normal file
@@ -0,0 +1,587 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: 400
|
||||
preferredHeight: 500
|
||||
panelKeyboardFocus: true
|
||||
|
||||
property string passwordSsid: ""
|
||||
property string passwordInput: ""
|
||||
property string expandedSsid: ""
|
||||
|
||||
onOpened: NetworkService.scan()
|
||||
|
||||
panelContent: Rectangle {
|
||||
color: Color.transparent
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
icon: Settings.data.network.wifiEnabled ? "wifi" : "wifi-off"
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.title")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: wifiSwitch
|
||||
checked: Settings.data.network.wifiEnabled
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.65 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: I18n.tr("tooltips.refresh")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
|
||||
onClicked: NetworkService.scan()
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Error message
|
||||
Rectangle {
|
||||
visible: NetworkService.lastError.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * scaling * 2)
|
||||
color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1)
|
||||
radius: Style.radiusS * scaling
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: Color.mError
|
||||
|
||||
RowLayout {
|
||||
id: errorRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "warning"
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mError
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NetworkService.lastError
|
||||
color: Color.mError
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
baseSize: Style.baseWidgetSize * 0.6
|
||||
onClicked: NetworkService.lastError = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main content area
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.transparent
|
||||
|
||||
// WiFi disabled state
|
||||
ColumnLayout {
|
||||
visible: !Settings.data.network.wifiEnabled
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "wifi-off"
|
||||
pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.disabled")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.enable-message")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Scanning state
|
||||
ColumnLayout {
|
||||
visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NBusyIndicator {
|
||||
running: true
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.searching")
|
||||
pointSize: Style.fontSizeNormal * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Networks list container
|
||||
NScrollView {
|
||||
visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0)
|
||||
anchors.fill: parent
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Network list
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Settings.data.network.wifiEnabled)
|
||||
return []
|
||||
|
||||
const nets = Object.values(NetworkService.networks)
|
||||
return nets.sort((a, b) => {
|
||||
if (a.connected !== b.connected)
|
||||
return b.connected - a.connected
|
||||
return b.signal - a.signal
|
||||
})
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2)
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
// Add opacity for operations in progress
|
||||
opacity: (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0
|
||||
|
||||
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
|
||||
|
||||
// Smooth opacity animation
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: netColumn
|
||||
width: parent.width - (Style.marginM * scaling * 2)
|
||||
x: Style.marginM * scaling
|
||||
y: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Main row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: NetworkService.signalIcon(modelData.signal)
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2 * scaling
|
||||
|
||||
NText {
|
||||
text: modelData.ssid
|
||||
pointSize: Style.fontSizeNormal * scaling
|
||||
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NText {
|
||||
text: I18n.tr("system.signal-strength", {
|
||||
"signal": modelData.signal
|
||||
})
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "•"
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: Style.marginXXS * scaling
|
||||
}
|
||||
|
||||
// Update the status badges area (around line 237)
|
||||
Rectangle {
|
||||
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
color: Color.mPrimary
|
||||
radius: height * 0.5
|
||||
width: connectedText.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||
|
||||
NText {
|
||||
id: connectedText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("wifi.panel.connected")
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: NetworkService.disconnectingFrom === modelData.ssid
|
||||
color: Color.mError
|
||||
radius: height * 0.5
|
||||
width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||
|
||||
NText {
|
||||
id: disconnectingText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("wifi.panel.disconnecting")
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: NetworkService.forgettingNetwork === modelData.ssid
|
||||
color: Color.mError
|
||||
radius: height * 0.5
|
||||
width: forgettingText.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: forgettingText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||
|
||||
NText {
|
||||
id: forgettingText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("wifi.panel.forgetting")
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
color: Color.transparent
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: height * 0.5
|
||||
width: savedText.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: savedText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||
|
||||
NText {
|
||||
id: savedText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("wifi.panel.saved")
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action area
|
||||
RowLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NBusyIndicator {
|
||||
visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid
|
||||
running: visible
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize * 0.5 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
icon: "trash"
|
||||
tooltipText: I18n.tr("tooltips.forget-network")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
text: {
|
||||
if (modelData.existing || modelData.cached)
|
||||
return I18n.tr("wifi.panel.connect")
|
||||
if (!NetworkService.isSecured(modelData.security))
|
||||
return I18n.tr("wifi.panel.connect")
|
||||
return I18n.tr("wifi.panel.password")
|
||||
}
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
enabled: !NetworkService.connecting
|
||||
onClicked: {
|
||||
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
|
||||
NetworkService.connect(modelData.ssid)
|
||||
} else {
|
||||
passwordSsid = modelData.ssid
|
||||
passwordInput = ""
|
||||
expandedSsid = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
text: I18n.tr("wifi.panel.disconnect")
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
onClicked: NetworkService.disconnect(modelData.ssid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Password input
|
||||
Rectangle {
|
||||
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
|
||||
Layout.fillWidth: true
|
||||
height: passwordRow.implicitHeight + Style.marginS * scaling * 2
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusS * scaling
|
||||
|
||||
RowLayout {
|
||||
id: passwordRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Style.radiusXS * scaling
|
||||
color: Color.mSurface
|
||||
border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
TextInput {
|
||||
id: pwdInput
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Style.marginS * scaling
|
||||
text: passwordInput
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurface
|
||||
echoMode: TextInput.Password
|
||||
selectByMouse: true
|
||||
focus: visible
|
||||
passwordCharacter: "●"
|
||||
onTextChanged: passwordInput = text
|
||||
onVisibleChanged: if (visible)
|
||||
forceActiveFocus()
|
||||
onAccepted: {
|
||||
if (text && !NetworkService.connecting) {
|
||||
NetworkService.connect(passwordSsid, text)
|
||||
passwordSsid = ""
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: parent.text.length === 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("wifi.panel.enter-password")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("wifi.panel.connect")
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
enabled: passwordInput.length > 0 && !NetworkService.connecting
|
||||
outlined: true
|
||||
onClicked: {
|
||||
NetworkService.connect(passwordSsid, passwordInput)
|
||||
passwordSsid = ""
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
passwordSsid = ""
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forget network
|
||||
Rectangle {
|
||||
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
|
||||
Layout.fillWidth: true
|
||||
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusS * scaling
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: Color.mOutline
|
||||
|
||||
RowLayout {
|
||||
id: forgetRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
RowLayout {
|
||||
NIcon {
|
||||
icon: "trash"
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mError
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.forget-network")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mError
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: forgetButton
|
||||
text: I18n.tr("wifi.panel.forget")
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
outlined: forgetButton.hovered ? false : true
|
||||
onClicked: {
|
||||
NetworkService.forget(modelData.ssid)
|
||||
expandedSsid = ""
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: expandedSsid = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state when no networks
|
||||
ColumnLayout {
|
||||
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "search"
|
||||
pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.no-networks")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("wifi.panel.scan-again")
|
||||
icon: "refresh"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: NetworkService.scan()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,95 +2,146 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property bool showingFullTitle: false
|
||||
property int lastWindowIndex: -1
|
||||
property real scaling: 1.0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: getTitle() !== ""
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Timer to hide full title after window switch
|
||||
Timer {
|
||||
id: fullTitleTimer
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
showingFullTitle = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update text when window changes
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onActiveWindowChanged() {
|
||||
// Check if window actually changed
|
||||
if (CompositorService.focusedWindowIndex !== lastWindowIndex) {
|
||||
lastWindowIndex = CompositorService.focusedWindowIndex
|
||||
showingFullTitle = true
|
||||
fullTitleTimer.restart()
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function getTitle() {
|
||||
// Use the service's focusedWindowTitle property which is updated immediately
|
||||
// when WindowOpenedOrChanged events are received
|
||||
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||
readonly property bool hasActiveWindow: CompositorService.getFocusedWindowTitle() !== ""
|
||||
readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window"
|
||||
readonly property string fallbackIcon: "user-desktop"
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
// Widget settings - matching MediaMini pattern
|
||||
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
|
||||
readonly property bool autoHide: (widgetSettings.autoHide !== undefined) ? widgetSettings.autoHide : widgetMetadata.autoHide
|
||||
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : (widgetMetadata.scrollingMode !== undefined ? widgetMetadata.scrollingMode : "hover")
|
||||
|
||||
// Fixed width
|
||||
readonly property real widgetWidth: Math.max(145, screen.width * 0.06)
|
||||
|
||||
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
|
||||
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)) : 0
|
||||
|
||||
opacity: !autoHide || hasActiveWindow ? 1.0 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
function calculatedVerticalHeight() {
|
||||
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
|
||||
}
|
||||
|
||||
function getAppIcon() {
|
||||
const focusedWindow = CompositorService.getFocusedWindow()
|
||||
if (!focusedWindow || !focusedWindow.appId)
|
||||
return ""
|
||||
try {
|
||||
// Try CompositorService first
|
||||
const focusedWindow = CompositorService.getFocusedWindow()
|
||||
if (focusedWindow && focusedWindow.appId) {
|
||||
try {
|
||||
const idValue = focusedWindow.appId
|
||||
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
|
||||
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase())
|
||||
if (iconResult && iconResult !== "") {
|
||||
return iconResult
|
||||
}
|
||||
} catch (iconError) {
|
||||
Logger.warn("ActiveWindow", "Error getting icon from CompositorService:", iconError)
|
||||
}
|
||||
}
|
||||
|
||||
return Icons.iconForAppId(focusedWindow.appId)
|
||||
if (CompositorService.isHyprland) {
|
||||
// Fallback to ToplevelManager
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel) {
|
||||
try {
|
||||
const activeToplevel = ToplevelManager.activeToplevel
|
||||
if (activeToplevel.appId) {
|
||||
const idValue2 = activeToplevel.appId
|
||||
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
|
||||
const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase())
|
||||
if (iconResult2 && iconResult2 !== "") {
|
||||
return iconResult2
|
||||
}
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ThemeIcons.iconFromName(fallbackIcon)
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
|
||||
return ThemeIcons.iconFromName(fallbackIcon)
|
||||
}
|
||||
}
|
||||
|
||||
// A hidden text element to safely measure the full title width
|
||||
// Hidden text element to measure full title width
|
||||
NText {
|
||||
id: fullTitleMetrics
|
||||
visible: false
|
||||
text: titleText.text
|
||||
font: titleText.font
|
||||
text: windowTitle
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
id: windowActiveRect
|
||||
visible: root.visible
|
||||
width: row.width + Style.marginM * scaling * 2
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)
|
||||
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
|
||||
Row {
|
||||
id: row
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginXS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
z: 1
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
width: Style.fontSizeL * scaling * 1.2
|
||||
height: Style.fontSizeL * scaling * 1.2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
|
||||
Layout.preferredWidth: Math.round(18 * scaling)
|
||||
Layout.preferredHeight: Math.round(18 * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showIcon
|
||||
|
||||
IconImage {
|
||||
id: windowIcon
|
||||
@@ -102,23 +153,138 @@ Row {
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
// Title container with scrolling
|
||||
Item {
|
||||
id: titleContainer
|
||||
Layout.preferredWidth: {
|
||||
// Calculate available width based on other elements
|
||||
var iconWidth = (showIcon && windowIcon.visible ? (18 * scaling + Style.marginS * scaling) : 0)
|
||||
var totalMargins = Style.marginXXS * scaling * 2
|
||||
var availableWidth = mainContainer.width - iconWidth - totalMargins
|
||||
return Math.max(20 * scaling, availableWidth)
|
||||
}
|
||||
Layout.maximumWidth: Layout.preferredWidth
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredHeight: titleText.height
|
||||
|
||||
// If hovered or just switched window, show up to 400 pixels
|
||||
// If not hovered show up to 150 pixels
|
||||
width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||
400 * scaling) : Math.min(
|
||||
fullTitleMetrics.contentWidth, 150 * scaling)
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mSecondary
|
||||
clip: true
|
||||
|
||||
Behavior on width {
|
||||
property bool isScrolling: false
|
||||
property bool isResetting: false
|
||||
property real textWidth: fullTitleMetrics.contentWidth
|
||||
property real containerWidth: width
|
||||
property bool needsScrolling: textWidth > containerWidth
|
||||
|
||||
// Timer for "always" mode with delay
|
||||
Timer {
|
||||
id: scrollStartTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (scrollingMode === "always" && titleContainer.needsScrolling) {
|
||||
titleContainer.isScrolling = true
|
||||
titleContainer.isResetting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update scrolling state based on mode
|
||||
property var updateScrollingState: function () {
|
||||
if (scrollingMode === "never") {
|
||||
isScrolling = false
|
||||
isResetting = false
|
||||
} else if (scrollingMode === "always") {
|
||||
if (needsScrolling) {
|
||||
if (mouseArea.containsMouse) {
|
||||
isScrolling = false
|
||||
isResetting = true
|
||||
} else {
|
||||
scrollStartTimer.restart()
|
||||
}
|
||||
} else {
|
||||
scrollStartTimer.stop()
|
||||
isScrolling = false
|
||||
isResetting = false
|
||||
}
|
||||
} else if (scrollingMode === "hover") {
|
||||
if (mouseArea.containsMouse && needsScrolling) {
|
||||
isScrolling = true
|
||||
isResetting = false
|
||||
} else {
|
||||
isScrolling = false
|
||||
if (needsScrolling) {
|
||||
isResetting = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onWidthChanged: updateScrollingState()
|
||||
Component.onCompleted: updateScrollingState()
|
||||
|
||||
// React to hover changes
|
||||
Connections {
|
||||
target: mouseArea
|
||||
function onContainsMouseChanged() {
|
||||
titleContainer.updateScrollingState()
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolling content with seamless loop
|
||||
Item {
|
||||
id: scrollContainer
|
||||
height: parent.height
|
||||
width: childrenRect.width
|
||||
|
||||
property real scrollX: 0
|
||||
x: scrollX
|
||||
|
||||
RowLayout {
|
||||
spacing: 50 * scaling // Gap between text copies
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
text: windowTitle
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
// Second copy for seamless scrolling
|
||||
NText {
|
||||
text: windowTitle
|
||||
font: titleText.font
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mOnSurface
|
||||
visible: titleContainer.needsScrolling && titleContainer.isScrolling
|
||||
}
|
||||
}
|
||||
|
||||
// Reset animation
|
||||
NumberAnimation on scrollX {
|
||||
running: titleContainer.isResetting
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.OutQuad
|
||||
onFinished: {
|
||||
titleContainer.isResetting = false
|
||||
}
|
||||
}
|
||||
|
||||
// Seamless infinite scroll
|
||||
NumberAnimation on scrollX {
|
||||
id: infiniteScroll
|
||||
running: titleContainer.isScrolling && !titleContainer.isResetting
|
||||
from: 0
|
||||
to: -(titleContainer.textWidth + 50 * scaling)
|
||||
duration: Math.max(4000, windowTitle.length * 100)
|
||||
loops: Animation.Infinite
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.InOutCubic
|
||||
@@ -127,12 +293,68 @@ Row {
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars - icon only
|
||||
Item {
|
||||
id: verticalLayout
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.marginM * scaling * 2
|
||||
height: parent.height - Style.marginM * scaling * 2
|
||||
visible: barPosition === "left" || barPosition === "right"
|
||||
z: 1
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: Style.baseWidgetSize * 0.5 * scaling
|
||||
anchors.centerIn: parent
|
||||
visible: windowTitle !== ""
|
||||
|
||||
IconImage {
|
||||
id: windowIconVertical
|
||||
anchors.fill: parent
|
||||
source: getAppIcon()
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area for hover detection
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onEntered: {
|
||||
if ((windowTitle !== "") && (barPosition === "left" || barPosition === "right") || (scrollingMode === "never")) {
|
||||
TooltipService.show(root, windowTitle, BarService.getTooltipDirection())
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onActiveWindowChanged() {
|
||||
try {
|
||||
windowIcon.source = Qt.binding(getAppIcon)
|
||||
windowIconVertical.source = Qt.binding(getAppIcon)
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
|
||||
}
|
||||
}
|
||||
function onWindowListChanged() {
|
||||
try {
|
||||
windowIcon.source = Qt.binding(getAppIcon)
|
||||
windowIconVertical.source = Qt.binding(getAppIcon)
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
// Enhanced icon states with better visual feedback
|
||||
icon: {
|
||||
if (ArchUpdaterService.busy)
|
||||
return "sync"
|
||||
if (ArchUpdaterService.updatePackages.length > 0) {
|
||||
// Show different icons based on update count
|
||||
const count = ArchUpdaterService.updatePackages.length
|
||||
if (count > 50)
|
||||
return "system_update_alt" // Many updates
|
||||
if (count > 10)
|
||||
return "system_update" // Moderate updates
|
||||
return "system_update" // Few updates
|
||||
}
|
||||
return "task_alt"
|
||||
}
|
||||
|
||||
// Enhanced tooltip with more information
|
||||
tooltipText: {
|
||||
if (ArchUpdaterService.busy)
|
||||
return "Checking for updates…"
|
||||
|
||||
var count = ArchUpdaterService.updatePackages.length
|
||||
if (count === 0)
|
||||
return "System is up to date ✓"
|
||||
|
||||
var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:")
|
||||
|
||||
var list = ArchUpdaterService.updatePackages || []
|
||||
var s = ""
|
||||
var limit = Math.min(list.length, 8)
|
||||
// Reduced to 8 for better readability
|
||||
for (var i = 0; i < limit; ++i) {
|
||||
var p = list[i]
|
||||
s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion)
|
||||
}
|
||||
if (list.length > 8)
|
||||
s += "\n… and " + (list.length - 8) + " more"
|
||||
|
||||
return header + "\n\n" + s + "\n\nClick to update system"
|
||||
}
|
||||
|
||||
// Enhanced click behavior with confirmation
|
||||
onClicked: {
|
||||
if (ArchUpdaterService.busy)
|
||||
return
|
||||
|
||||
if (ArchUpdaterService.updatePackages.length > 0) {
|
||||
// Show confirmation dialog for updates
|
||||
PanelService.getPanel("archUpdaterPanel").toggle(screen)
|
||||
} else {
|
||||
// Just refresh if no updates available
|
||||
ArchUpdaterService.doPoll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,97 +5,125 @@ import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
|
||||
|
||||
// Test mode
|
||||
readonly property bool testMode: false
|
||||
readonly property int testPercent: 100
|
||||
readonly property bool testCharging: false
|
||||
|
||||
// Main properties
|
||||
readonly property var battery: UPower.displayDevice
|
||||
readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||
readonly property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||
readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||
property bool hasNotifiedLowBattery: false
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
// Helper to evaluate and possibly notify
|
||||
function maybeNotify(percent, charging) {
|
||||
// Only notify once we are a below threshold
|
||||
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
|
||||
root.hasNotifiedLowBattery = true
|
||||
ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", {
|
||||
"percent": Math.round(percent)
|
||||
}))
|
||||
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
|
||||
// Reset when charging starts or when battery recovers 5% above threshold
|
||||
root.hasNotifiedLowBattery = false
|
||||
}
|
||||
}
|
||||
|
||||
// Test mode
|
||||
property bool testMode: false
|
||||
property int testPercent: 49
|
||||
property bool testCharging: false
|
||||
|
||||
property var battery: UPower.displayDevice
|
||||
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||
|
||||
// Choose icon based on charge and charging state
|
||||
function batteryIcon() {
|
||||
|
||||
if (!isReady || !battery.isLaptopBattery)
|
||||
return "battery_android_alert"
|
||||
|
||||
if (charging)
|
||||
return "battery_android_bolt"
|
||||
|
||||
if (percent >= 95)
|
||||
return "battery_android_full"
|
||||
|
||||
// Hardcoded battery symbols
|
||||
if (percent >= 85)
|
||||
return "battery_android_6"
|
||||
if (percent >= 70)
|
||||
return "battery_android_5"
|
||||
if (percent >= 55)
|
||||
return "battery_android_4"
|
||||
if (percent >= 40)
|
||||
return "battery_android_3"
|
||||
if (percent >= 25)
|
||||
return "battery_android_2"
|
||||
if (percent >= 10)
|
||||
return "battery_android_1"
|
||||
if (percent >= 0)
|
||||
return "battery_android_0"
|
||||
// Watch for battery changes
|
||||
Connections {
|
||||
target: UPower.displayDevice
|
||||
function onPercentageChanged() {
|
||||
var currentPercent = UPower.displayDevice.percentage * 100
|
||||
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
|
||||
root.maybeNotify(currentPercent, isCharging)
|
||||
}
|
||||
|
||||
icon: batteryIcon()
|
||||
text: (isReady && battery.isLaptopBattery) ? Math.round(percent) + "%" : "-"
|
||||
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
||||
forceOpen: isReady && battery.isLaptopBattery && Settings.data.bar.alwaysShowBatteryPercentage
|
||||
disableOpen: (!isReady || !battery.isLaptopBattery)
|
||||
function onStateChanged() {
|
||||
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
|
||||
// Reset notification flag when charging starts
|
||||
if (isCharging) {
|
||||
root.hasNotifiedLowBattery = false
|
||||
}
|
||||
// Also re-evaluate maybeNotify, as state might have changed
|
||||
var currentPercent = UPower.displayDevice.percentage * 100
|
||||
root.maybeNotify(currentPercent, isCharging)
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
|
||||
text: (isReady || testMode) ? Math.round(percent) : "-"
|
||||
suffix: "%"
|
||||
autoHide: false
|
||||
forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))
|
||||
tooltipText: {
|
||||
let lines = []
|
||||
|
||||
if (testMode) {
|
||||
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(12345))
|
||||
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
if (!isReady || !battery.isLaptopBattery) {
|
||||
return "No Battery Detected"
|
||||
return "No battery detected."
|
||||
}
|
||||
|
||||
if (battery.timeToEmpty > 0) {
|
||||
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
|
||||
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`)
|
||||
}
|
||||
|
||||
if (battery.timeToFull > 0) {
|
||||
lines.push("Time Until Full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
|
||||
lines.push(`Time until full: ${Time.formatVagueHumanReadableDuration(battery.timeToFull)}.`)
|
||||
}
|
||||
|
||||
if (battery.changeRate !== undefined) {
|
||||
const rate = battery.changeRate
|
||||
if (rate > 0) {
|
||||
lines.push(charging ? "Charging Rate: " + rate.toFixed(2) + " W" : "Discharging Rate: " + rate.toFixed(
|
||||
2) + " W")
|
||||
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(2) + " W.")
|
||||
} else if (rate < 0) {
|
||||
lines.push("Discharging Rate: " + Math.abs(rate).toFixed(2) + " W")
|
||||
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.")
|
||||
} else {
|
||||
lines.push("Estimating...")
|
||||
}
|
||||
} else {
|
||||
lines.push(charging ? "Charging" : "Discharging")
|
||||
lines.push(charging ? "Charging." : "Discharging.")
|
||||
}
|
||||
|
||||
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
|
||||
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
|
||||
}
|
||||
|
||||
@@ -11,25 +11,17 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
visible: Settings.data.network.bluetoothEnabled
|
||||
sizeMultiplier: 0.8
|
||||
colorBg: Color.mSurfaceVariant
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
colorBg: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: {
|
||||
// Show different icons based on connection status
|
||||
if (BluetoothService.pairedDevices.length > 0) {
|
||||
return "bluetooth_connected"
|
||||
} else if (BluetoothService.discovering) {
|
||||
return "bluetooth_searching"
|
||||
} else {
|
||||
return "bluetooth"
|
||||
}
|
||||
}
|
||||
tooltipText: "Bluetooth Devices"
|
||||
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen)
|
||||
tooltipText: I18n.tr("tooltips.bluetooth-devices")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
|
||||
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -9,7 +10,27 @@ Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstBrightnessReceived: false
|
||||
@@ -25,8 +46,7 @@ Item {
|
||||
function getIcon() {
|
||||
var monitor = getMonitor()
|
||||
var brightness = monitor ? monitor.brightness : 0
|
||||
return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness
|
||||
< 0.66 ? "brightness_medium" : "brightness_high"
|
||||
return brightness <= 0.5 ? "brightness-low" : "brightness-high"
|
||||
}
|
||||
|
||||
// Connection used to open the pill when brightness changes
|
||||
@@ -34,44 +54,46 @@ Item {
|
||||
target: getMonitor()
|
||||
ignoreUnknownSignals: true
|
||||
function onBrightnessUpdated() {
|
||||
Logger.log("Bar-Brightness", "OnBrightnessUpdated")
|
||||
var monitor = getMonitor()
|
||||
if (!monitor)
|
||||
return
|
||||
var currentBrightness = monitor.brightness
|
||||
|
||||
// Ignore if this is the first time or if brightness hasn't actually changed
|
||||
// Ignore if this is the first time we receive an update.
|
||||
// Most likely service just kicked off.
|
||||
if (!firstBrightnessReceived) {
|
||||
firstBrightnessReceived = true
|
||||
monitor.lastBrightness = currentBrightness
|
||||
return
|
||||
}
|
||||
|
||||
// Only show pill if brightness actually changed (not just loaded from settings)
|
||||
if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) {
|
||||
pill.show()
|
||||
}
|
||||
|
||||
monitor.lastBrightness = currentBrightness
|
||||
pill.show()
|
||||
hideTimerAfterChange.restart()
|
||||
}
|
||||
}
|
||||
|
||||
NPill {
|
||||
Timer {
|
||||
id: hideTimerAfterChange
|
||||
interval: 2500
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: pill.hide()
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: getIcon()
|
||||
iconCircleColor: Color.mPrimary
|
||||
collapsedIconColor: Color.mOnSurface
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: {
|
||||
var monitor = getMonitor()
|
||||
return monitor ? (Math.round(monitor.brightness * 100) + "%") : ""
|
||||
return monitor ? Math.round(monitor.brightness * 100) : ""
|
||||
}
|
||||
suffix: text.length > 0 ? "%" : "-"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: {
|
||||
var monitor = getMonitor()
|
||||
if (!monitor)
|
||||
return ""
|
||||
return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method
|
||||
+ "\nLeft click for advanced settings.\nScroll up/down to change brightness."
|
||||
return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nRight click for settings.\nScroll to modify brightness."
|
||||
}
|
||||
|
||||
onWheel: function (angle) {
|
||||
@@ -86,8 +108,15 @@ Item {
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
|
||||
settingsPanel.open(screen)
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Display
|
||||
settingsPanel.open()
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Display
|
||||
settingsPanel.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
@@ -8,37 +9,121 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
implicitWidth: clock.width + Style.marginM * 2 * scaling
|
||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Clock Icon with attached calendar
|
||||
NClock {
|
||||
id: clock
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: Time.dateString
|
||||
target: clock
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
readonly property var now: Time.date
|
||||
|
||||
// Resolve settings: try user settings or defaults from BarWidgetRegistry
|
||||
readonly property bool usePrimaryColor: widgetSettings.usePrimaryColor !== undefined ? widgetSettings.usePrimaryColor : widgetMetadata.usePrimaryColor
|
||||
readonly property bool useCustomFont: widgetSettings.useCustomFont !== undefined ? widgetSettings.useCustomFont : widgetMetadata.useCustomFont
|
||||
readonly property string customFont: widgetSettings.customFont !== undefined ? widgetSettings.customFont : widgetMetadata.customFont
|
||||
readonly property string formatHorizontal: widgetSettings.formatHorizontal !== undefined ? widgetSettings.formatHorizontal : widgetMetadata.formatHorizontal
|
||||
readonly property string formatVertical: widgetSettings.formatVertical !== undefined ? widgetSettings.formatVertical : widgetMetadata.formatVertical
|
||||
|
||||
implicitWidth: isBarVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.marginM * 2 * scaling)
|
||||
|
||||
implicitHeight: isBarVertical ? Math.round(verticalLoader.implicitHeight + Style.marginS * 2 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
|
||||
radius: Math.round(Style.radiusS * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Horizontal
|
||||
Loader {
|
||||
id: horizontalLoader
|
||||
active: !isBarVertical
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Settings.data.bar.showCapsule ? -4 * scaling : -2 * scaling
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: Qt.locale().toString(now, formatHorizontal.trim()).split("\\n")
|
||||
NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
|
||||
pointSize: {
|
||||
if (repeater.model.length == 1) {
|
||||
return Style.fontSizeS * scaling
|
||||
} else {
|
||||
return (index == 0) ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
|
||||
}
|
||||
}
|
||||
font.weight: Style.fontWeightBold
|
||||
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical
|
||||
Loader {
|
||||
id: verticalLoader
|
||||
active: isBarVertical
|
||||
anchors.centerIn: parent // Now this works without layout conflicts
|
||||
sourceComponent: ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: -2 * scaling
|
||||
Repeater {
|
||||
model: Qt.locale().toString(now, formatVertical.trim()).split(" ")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clockMouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
if (!PanelService.getPanel("calendarPanel")?.active) {
|
||||
tooltip.show()
|
||||
TooltipService.show(root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection())
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
tooltip.hide()
|
||||
TooltipService.hide()
|
||||
}
|
||||
onClicked: {
|
||||
tooltip.hide()
|
||||
PanelService.getPanel("calendarPanel")?.toggle(screen)
|
||||
TooltipService.hide()
|
||||
PanelService.getPanel("calendarPanel")?.toggle(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
Modules/Bar/Widgets/ControlCenter.qml
Normal file
@@ -0,0 +1,66 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Effects
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
|
||||
readonly property bool useDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
readonly property string customIconPath: widgetSettings.customIconPath || ""
|
||||
|
||||
// If we have a custom path or distro logo, don't use the theme icon.
|
||||
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
|
||||
tooltipText: I18n.tr("tooltips.open-control-center")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mOnSurface
|
||||
colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent
|
||||
onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
|
||||
|
||||
IconImage {
|
||||
id: customOrDistroLogo
|
||||
anchors.centerIn: parent
|
||||
width: root.width * 0.8
|
||||
height: width
|
||||
source: {
|
||||
if (customIconPath !== "")
|
||||
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath
|
||||
if (useDistroLogo)
|
||||
return DistroLogoService.osLogo
|
||||
return ""
|
||||
}
|
||||
visible: source !== ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
140
Modules/Bar/Widgets/CustomButton.qml
Normal file
@@ -0,0 +1,140 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Widget properties passed from Bar.qml
|
||||
property var screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// Use settings or defaults from BarWidgetRegistry
|
||||
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
|
||||
readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
|
||||
readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
|
||||
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
|
||||
readonly property string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "")
|
||||
readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000)
|
||||
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: customIcon
|
||||
text: _dynamicText
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
autoHide: false
|
||||
forceOpen: _dynamicText !== ""
|
||||
forceClose: false
|
||||
disableOpen: true
|
||||
tooltipText: {
|
||||
if (!hasExec) {
|
||||
return "Custom button, configure in settings."
|
||||
} else {
|
||||
var lines = []
|
||||
if (leftClickExec !== "") {
|
||||
lines.push(`Left click: ${leftClickExec}.`)
|
||||
}
|
||||
if (rightClickExec !== "") {
|
||||
lines.push(`Right click: ${rightClickExec}.`)
|
||||
}
|
||||
if (middleClickExec !== "") {
|
||||
lines.push(`Middle click: ${middleClickExec}.`)
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.onClicked()
|
||||
onRightClicked: root.onRightClicked()
|
||||
onMiddleClicked: root.onMiddleClicked()
|
||||
}
|
||||
|
||||
// Internal state for dynamic text
|
||||
property string _dynamicText: ""
|
||||
|
||||
// Periodically run the text command (if set)
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: Math.max(250, textIntervalMs)
|
||||
repeat: true
|
||||
running: (textCommand && textCommand.length > 0)
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
if (!textCommand || textCommand.length === 0)
|
||||
return
|
||||
if (textProc.running)
|
||||
return
|
||||
textProc.command = ["sh", "-lc", textCommand]
|
||||
textProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: textProc
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
var out = String(stdout.text || "").trim()
|
||||
if (out.indexOf("\n") !== -1) {
|
||||
out = out.split("\n")[0]
|
||||
}
|
||||
_dynamicText = out
|
||||
}
|
||||
}
|
||||
|
||||
function onClicked() {
|
||||
if (leftClickExec) {
|
||||
Quickshell.execDetached(["sh", "-c", leftClickExec])
|
||||
Logger.log("CustomButton", `Executing command: ${leftClickExec}`)
|
||||
} else if (!hasExec) {
|
||||
// No script was defined, open settings
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Bar
|
||||
settingsPanel.open()
|
||||
}
|
||||
}
|
||||
|
||||
function onRightClicked() {
|
||||
if (rightClickExec) {
|
||||
Quickshell.execDetached(["sh", "-c", rightClickExec])
|
||||
Logger.log("CustomButton", `Executing command: ${rightClickExec}`)
|
||||
}
|
||||
}
|
||||
|
||||
function onMiddleClicked() {
|
||||
if (middleClickExec) {
|
||||
Quickshell.execDetached(["sh", "-c", middleClickExec])
|
||||
Logger.log("CustomButton", `Executing command: ${middleClickExec}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Modules/Bar/Widgets/DarkMode.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
icon: "dark-mode"
|
||||
tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: Settings.data.colorSchemes.darkMode ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
|
||||
colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
|
||||
}
|
||||
24
Modules/Bar/Widgets/KeepAwake.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: IdleInhibitorService.manualToggle()
|
||||
}
|
||||
@@ -1,36 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
|
||||
// Use the shared service for keyboard layout
|
||||
property string currentLayout: KeyboardLayoutService.currentLayout
|
||||
|
||||
width: pill.width
|
||||
height: pill.height
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPill {
|
||||
BarPill {
|
||||
id: pill
|
||||
icon: "keyboard_alt"
|
||||
iconCircleColor: Color.mPrimary
|
||||
collapsedIconColor: Color.mOnSurface
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: currentLayout
|
||||
tooltipText: "Keyboard Layout: " + currentLayout
|
||||
|
||||
screen: root.screen
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: "keyboard"
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: currentLayout.toUpperCase()
|
||||
tooltipText: I18n.tr("tooltips.keyboard-layout", {
|
||||
"layout": currentLayout.toUpperCase()
|
||||
})
|
||||
forceOpen: root.displayMode === "forceOpen"
|
||||
forceClose: root.displayMode === "alwaysHide"
|
||||
onClicked: {
|
||||
|
||||
// You could open keyboard settings here if needed
|
||||
// For now, just show the current layout
|
||||
// You could open keyboard settings here if needed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,78 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
readonly property bool autoHide: (widgetSettings.autoHide !== undefined) ? widgetSettings.autoHide : widgetMetadata.autoHide
|
||||
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
|
||||
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
|
||||
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : widgetMetadata.scrollingMode
|
||||
|
||||
// Fixed width - no expansion
|
||||
readonly property real widgetWidth: Math.max(145, screen.width * 0.06)
|
||||
|
||||
readonly property bool hasActivePlayer: MediaService.currentPlayer !== null && getTitle() !== ""
|
||||
readonly property string placeholderText: I18n.tr("bar.widget-settings.media-mini.no-active-player")
|
||||
|
||||
readonly property string tooltipText: {
|
||||
var title = getTitle()
|
||||
var controls = ""
|
||||
if (MediaService.canGoNext) {
|
||||
controls += "Right click for next.\n"
|
||||
}
|
||||
if (MediaService.canGoPrevious) {
|
||||
controls += "Middle click for previous."
|
||||
}
|
||||
if (controls !== "") {
|
||||
return title + "\n\n" + controls
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
|
||||
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)) : 0
|
||||
|
||||
opacity: !autoHide || hasActivePlayer || (!hasActivePlayer && !autoHide) ? 1.0 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
function getTitle() {
|
||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||
}
|
||||
|
||||
function calculatedVerticalHeight() {
|
||||
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
|
||||
}
|
||||
|
||||
// A hidden text element to safely measure the full title width
|
||||
NText {
|
||||
id: fullTitleMetrics
|
||||
@@ -31,137 +88,245 @@ Row {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
width: row.width + Style.marginM * scaling * 2
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
id: mediaMini
|
||||
visible: root.visible
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)
|
||||
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
|
||||
&& MediaService.isPlaying
|
||||
active: showVisualizer && visualizerType == "linear"
|
||||
z: 0
|
||||
|
||||
sourceComponent: LinearSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: 20 * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
|
||||
&& MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: MirroredSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
|
||||
&& MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginXS * scaling
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "mirrored"
|
||||
z: 0
|
||||
|
||||
sourceComponent: MirroredSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "wave"
|
||||
z: 0
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: (barPosition === "top" || barPosition === "bottom")
|
||||
z: 1 // Above the visualizer
|
||||
|
||||
NIcon {
|
||||
id: windowIcon
|
||||
text: MediaService.isPlaying ? "pause" : "play_arrow"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
icon: hasActivePlayer ? (MediaService.isPlaying ? "media-pause" : "media-play") : "disc"
|
||||
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !hasActivePlayer || (!showAlbumArt && !trackArt.visible)
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Settings.data.audio.showMiniplayerAlbumArt
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showAlbumArt && hasActivePlayer
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
width: 18 * scaling
|
||||
height: 18 * scaling
|
||||
radius: width * 0.5
|
||||
color: Color.transparent
|
||||
antialiasing: true
|
||||
clip: true
|
||||
Item {
|
||||
Layout.preferredWidth: Math.round(18 * scaling)
|
||||
Layout.preferredHeight: Math.round(18 * scaling)
|
||||
|
||||
NImageCircled {
|
||||
id: trackArt
|
||||
visible: MediaService.trackArtUrl.toString() !== ""
|
||||
anchors.fill: parent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: scaling
|
||||
imagePath: MediaService.trackArtUrl
|
||||
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
|
||||
fallbackIcon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
fallbackIconSize: 10 * scaling
|
||||
borderWidth: 0
|
||||
border.color: Color.transparent
|
||||
}
|
||||
|
||||
// Fallback icon when no album art available
|
||||
NIcon {
|
||||
id: windowIconFallback
|
||||
text: MediaService.isPlaying ? "pause" : "play_arrow"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: getTitle() !== "" && !trackArt.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
Item {
|
||||
id: titleContainer
|
||||
Layout.preferredWidth: {
|
||||
// Calculate available width based on other elements in the row
|
||||
var iconWidth = (windowIcon.visible ? (Style.fontSizeL * scaling + Style.marginS * scaling) : 0)
|
||||
var albumArtWidth = (hasActivePlayer && showAlbumArt ? (18 * scaling + Style.marginS * scaling) : 0)
|
||||
var totalMargins = Style.marginXXS * scaling * 2
|
||||
var availableWidth = mainContainer.width - iconWidth - albumArtWidth - totalMargins
|
||||
return Math.max(20 * scaling, availableWidth)
|
||||
}
|
||||
Layout.maximumWidth: Layout.preferredWidth
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredHeight: titleText.height
|
||||
|
||||
// If hovered or just switched window, show up to 400 pixels
|
||||
// If not hovered show up to 150 pixels
|
||||
width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||
400 * scaling) : Math.min(fullTitleMetrics.contentWidth,
|
||||
150 * scaling)
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mTertiary
|
||||
clip: true
|
||||
|
||||
Behavior on width {
|
||||
property bool isScrolling: false
|
||||
property bool isResetting: false
|
||||
property real textWidth: fullTitleMetrics.contentWidth
|
||||
property real containerWidth: 0
|
||||
property bool needsScrolling: textWidth > containerWidth
|
||||
|
||||
// Timer for "always" mode with delay
|
||||
Timer {
|
||||
id: scrollStartTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (scrollingMode === "always" && titleContainer.needsScrolling) {
|
||||
titleContainer.isScrolling = true
|
||||
titleContainer.isResetting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update scrolling state based on mode
|
||||
property var updateScrollingState: function () {
|
||||
if (scrollingMode === "never") {
|
||||
isScrolling = false
|
||||
isResetting = false
|
||||
} else if (scrollingMode === "always") {
|
||||
if (needsScrolling) {
|
||||
if (mouseArea.containsMouse) {
|
||||
isScrolling = false
|
||||
isResetting = true
|
||||
} else {
|
||||
scrollStartTimer.restart()
|
||||
}
|
||||
} else {
|
||||
scrollStartTimer.stop()
|
||||
isScrolling = false
|
||||
isResetting = false
|
||||
}
|
||||
} else if (scrollingMode === "hover") {
|
||||
if (mouseArea.containsMouse && needsScrolling) {
|
||||
isScrolling = true
|
||||
isResetting = false
|
||||
} else {
|
||||
isScrolling = false
|
||||
if (needsScrolling) {
|
||||
isResetting = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onWidthChanged: {
|
||||
containerWidth = width
|
||||
updateScrollingState()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
containerWidth = width
|
||||
updateScrollingState()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: mouseArea
|
||||
function onContainsMouseChanged() {
|
||||
titleContainer.updateScrollingState()
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolling content
|
||||
Item {
|
||||
id: scrollContainer
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
|
||||
property real scrollX: 0
|
||||
x: scrollX
|
||||
|
||||
RowLayout {
|
||||
spacing: 50 * scaling // Gap between text copies
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
text: hasActivePlayer ? getTitle() : placeholderText
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: hasActivePlayer ? Text.AlignLeft : Text.AlignHCenter
|
||||
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: hasActivePlayer ? getTitle() : placeholderText
|
||||
font: titleText.font
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: hasActivePlayer ? Text.AlignLeft : Text.AlignHCenter
|
||||
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
visible: hasActivePlayer && titleContainer.needsScrolling && titleContainer.isScrolling
|
||||
}
|
||||
}
|
||||
|
||||
// Reset animation
|
||||
NumberAnimation on scrollX {
|
||||
running: titleContainer.isResetting
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.OutQuad
|
||||
onFinished: {
|
||||
titleContainer.isResetting = false
|
||||
}
|
||||
}
|
||||
|
||||
// Seamless infinite scroll
|
||||
NumberAnimation on scrollX {
|
||||
id: infiniteScroll
|
||||
running: titleContainer.isScrolling && !titleContainer.isResetting
|
||||
from: 0
|
||||
to: -(titleContainer.textWidth + 50 * scaling) // Scroll one complete text width + gap
|
||||
duration: Math.max(4000, getTitle().length * 120)
|
||||
loops: Animation.Infinite
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.InOutCubic
|
||||
@@ -170,13 +335,67 @@ Row {
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars - icon only
|
||||
Item {
|
||||
id: verticalLayout
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.marginM * scaling * 2
|
||||
height: parent.height - Style.marginM * scaling * 2
|
||||
visible: barPosition === "left" || barPosition === "right"
|
||||
z: 1 // Above the visualizer
|
||||
|
||||
// Media icon
|
||||
Item {
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: Style.baseWidgetSize * 0.5 * scaling
|
||||
anchors.centerIn: parent
|
||||
|
||||
NIcon {
|
||||
id: mediaIconVertical
|
||||
anchors.fill: parent
|
||||
icon: hasActivePlayer ? (MediaService.isPlaying ? "media-pause" : "media-play") : "disc"
|
||||
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area for hover detection
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: MediaService.playPause()
|
||||
cursorShape: hasActivePlayer ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
if (!hasActivePlayer || !MediaService.currentPlayer || !MediaService.canPlay) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
MediaService.playPause()
|
||||
} else if (mouse.button == Qt.RightButton) {
|
||||
MediaService.next()
|
||||
// Need to hide the tooltip instantly
|
||||
tooltip.visible = false
|
||||
} else if (mouse.button == Qt.MiddleButton) {
|
||||
MediaService.previous()
|
||||
// Need to hide the tooltip instantly
|
||||
tooltip.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
var textToShow = hasActivePlayer ? tooltipText : placeholderText
|
||||
if ((textToShow !== "") && (barPosition === "left" || barPosition === "right") || (scrollingMode === "never")) {
|
||||
TooltipService.show(root, textToShow, BarService.getTooltipDirection())
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
128
Modules/Bar/Widgets/Microphone.qml
Normal file
@@ -0,0 +1,128 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstInputVolumeReceived: false
|
||||
property int wheelAccumulator: 0
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
function getIcon() {
|
||||
if (AudioService.inputMuted) {
|
||||
return "microphone-mute"
|
||||
}
|
||||
return (AudioService.inputVolume <= Number.EPSILON) ? "microphone-mute" : "microphone"
|
||||
}
|
||||
|
||||
// Connection used to open the pill when input volume changes
|
||||
Connections {
|
||||
target: AudioService.source?.audio ? AudioService.source?.audio : null
|
||||
function onVolumeChanged() {
|
||||
// Logger.log("Bar:Microphone", "onInputVolumeChanged")
|
||||
if (!firstInputVolumeReceived) {
|
||||
// Ignore the first volume change
|
||||
firstInputVolumeReceived = true
|
||||
} else {
|
||||
pill.show()
|
||||
externalHideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection used to open the pill when input mute state changes
|
||||
Connections {
|
||||
target: AudioService.source?.audio ? AudioService.source?.audio : null
|
||||
function onMutedChanged() {
|
||||
// Logger.log("Bar:Microphone", "onInputMutedChanged")
|
||||
if (!firstInputVolumeReceived) {
|
||||
// Ignore the first mute change
|
||||
firstInputVolumeReceived = true
|
||||
} else {
|
||||
pill.show()
|
||||
externalHideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: externalHideTimer
|
||||
running: false
|
||||
interval: 1500
|
||||
onTriggered: {
|
||||
pill.hide()
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: getIcon()
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.round(AudioService.inputVolume * 100)
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: I18n.tr("tooltips.microphone-volume-at", {
|
||||
"volume": Math.round(AudioService.inputVolume * 100)
|
||||
})
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
if (wheelAccumulator >= 120) {
|
||||
wheelAccumulator = 0
|
||||
AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume)
|
||||
} else if (wheelAccumulator <= -120) {
|
||||
wheelAccumulator = 0
|
||||
AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume)
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
AudioService.setInputMuted(!AudioService.inputMuted)
|
||||
}
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
|
||||
settingsPanel.open()
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["pwvucontrol"])
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Modules/Bar/Widgets/NightLight.qml
Normal file
@@ -0,0 +1,50 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: Settings.data.nightLight.forced ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Settings.data.nightLight.forced ? Color.mOnPrimary : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
|
||||
tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("tooltips.night-light-forced") : I18n.tr("tooltips.night-light-enabled")) : I18n.tr("tooltips.night-light-disabled")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
onClicked: {
|
||||
// Check if wlsunset is available before enabling night light
|
||||
if (!ProgramCheckerService.wlsunsetAvailable) {
|
||||
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!Settings.data.nightLight.enabled) {
|
||||
Settings.data.nightLight.enabled = true
|
||||
Settings.data.nightLight.forced = false
|
||||
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
|
||||
Settings.data.nightLight.forced = true
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false
|
||||
Settings.data.nightLight.forced = false
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Display
|
||||
settingsPanel.open()
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,79 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
icon: "notifications"
|
||||
tooltipText: "Notification History"
|
||||
colorBg: Color.mSurfaceVariant
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge
|
||||
readonly property bool hideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero
|
||||
|
||||
function lastSeenTs() {
|
||||
return Settings.data.notifications?.lastSeenTs || 0
|
||||
}
|
||||
|
||||
function computeUnreadCount() {
|
||||
var since = lastSeenTs()
|
||||
var count = 0
|
||||
var model = NotificationService.historyList
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var item = model.get(i)
|
||||
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp
|
||||
if (ts > since)
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.open-notification-history-disable-dnd") : I18n.tr("tooltips.open-notification-history-enable-dnd")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen)
|
||||
|
||||
onClicked: {
|
||||
var panel = PanelService.getPanel("notificationHistoryPanel")
|
||||
panel?.toggle(this)
|
||||
Settings.data.notifications.lastSeenTs = Time.timestamp * 1000
|
||||
}
|
||||
|
||||
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
|
||||
Loader {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: 2 * scaling
|
||||
anchors.topMargin: 1 * scaling
|
||||
z: 2
|
||||
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
|
||||
sourceComponent: Rectangle {
|
||||
id: badge
|
||||
readonly property int count: computeUnreadCount()
|
||||
height: 8 * scaling
|
||||
width: height
|
||||
radius: height / 2
|
||||
color: Color.mError
|
||||
border.color: Color.mSurface
|
||||
border.width: 1
|
||||
visible: count > 0 || !hideWhenZero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,51 +10,20 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property var powerProfiles: PowerProfiles
|
||||
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
|
||||
property real scaling: 1.0
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
visible: hasPP
|
||||
baseSize: Style.capsuleHeight
|
||||
visible: PowerProfileService.available
|
||||
|
||||
function profileIcon() {
|
||||
if (!hasPP)
|
||||
return "balance"
|
||||
if (powerProfiles.profile === PowerProfile.Performance)
|
||||
return "speed"
|
||||
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||
return "balance"
|
||||
if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||
return "eco"
|
||||
}
|
||||
|
||||
function profileName() {
|
||||
if (!hasPP)
|
||||
return "Unknown"
|
||||
if (powerProfiles.profile === PowerProfile.Performance)
|
||||
return "Performance"
|
||||
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||
return "Balanced"
|
||||
if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||
return "Power Saver"
|
||||
}
|
||||
|
||||
function changeProfile() {
|
||||
if (!hasPP)
|
||||
return
|
||||
if (powerProfiles.profile === PowerProfile.Performance)
|
||||
powerProfiles.profile = PowerProfile.PowerSaver
|
||||
else if (powerProfiles.profile === PowerProfile.Balanced)
|
||||
powerProfiles.profile = PowerProfile.Performance
|
||||
else if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||
powerProfiles.profile = PowerProfile.Balanced
|
||||
}
|
||||
|
||||
icon: root.profileIcon()
|
||||
tooltipText: root.profileName()
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
icon: PowerProfileService.getIcon()
|
||||
tooltipText: I18n.tr("tooltips.power-profile", {
|
||||
"profile": PowerProfileService.getName()
|
||||
})
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
|
||||
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: root.changeProfile()
|
||||
onClicked: PowerProfileService.cycleProfile()
|
||||
}
|
||||
|
||||
23
Modules/Bar/Widgets/ScreenRecorder.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Screen Recording Indicator
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
icon: "camera-video"
|
||||
tooltipText: ScreenRecorderService.isRecording ? I18n.tr("tooltips.click-to-stop-recording") : I18n.tr("tooltips.click-to-start-recording")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Screen Recording Indicator
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
visible: ScreenRecorderService.isRecording
|
||||
icon: "videocam"
|
||||
tooltipText: "Screen Recording Active\nClick To Stop Recording"
|
||||
sizeMultiplier: 0.8
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
24
Modules/Bar/Widgets/SessionMenu.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
icon: "power"
|
||||
tooltipText: I18n.tr("tooltips.session-menu")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mError
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle()
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
icon: "widgets"
|
||||
tooltipText: "Open Side Panel"
|
||||
sizeMultiplier: 0.8
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen)
|
||||
}
|
||||
40
Modules/Bar/Widgets/Spacer.qml
Normal file
@@ -0,0 +1,40 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Widget properties passed from Bar.qml
|
||||
property var screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// Use settings or defaults from BarWidgetRegistry
|
||||
readonly property int spacerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
|
||||
|
||||
// Set the width based on user settings
|
||||
implicitWidth: spacerWidth * scaling
|
||||
implicitHeight: Style.barHeight * scaling
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
}
|
||||
@@ -1,98 +1,335 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
width: row.width + Style.marginM * scaling * 2
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
|
||||
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
|
||||
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
|
||||
readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
|
||||
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
|
||||
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
|
||||
|
||||
readonly property real iconSize: textSize * 1.4
|
||||
readonly property real textSize: {
|
||||
var base = isVertical ? width * 0.82 : height
|
||||
return Math.max(1, compact ? base * 0.43 : base * 0.33)
|
||||
}
|
||||
|
||||
readonly property int percentTextWidth: Math.ceil(percentMetrics.tightBoundingRect.width + 2)
|
||||
readonly property int tempTextWidth: Math.ceil(tempMetrics.tightBoundingRect.width + 2)
|
||||
readonly property int memTextWidth: Math.ceil(memMetrics.tightBoundingRect.width + 2)
|
||||
|
||||
TextMetrics {
|
||||
id: percentMetrics
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: Style.fontWeightMedium
|
||||
font.pointSize: textSize * Settings.data.ui.fontFixedScale
|
||||
text: "99%" // Use the longest possible string for measurement
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: tempMetrics
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: Style.fontWeightMedium
|
||||
font.pointSize: textSize * Settings.data.ui.fontFixedScale
|
||||
text: "99°" // Use the longest possible string for measurement
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: memMetrics
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: Style.fontWeightMedium
|
||||
font.pointSize: textSize * Settings.data.ui.fontFixedScale
|
||||
text: "99.9K" // Longest value part of network speed
|
||||
}
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round(mainGrid.implicitWidth + Style.marginM * 2 * scaling)
|
||||
implicitHeight: isVertical ? Math.round(mainGrid.implicitHeight + Style.marginM * 2 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
GridLayout {
|
||||
id: mainGrid
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? -1 : 1
|
||||
columns: isVertical ? 1 : -1
|
||||
rowSpacing: isVertical ? (Style.marginM * scaling) : 0
|
||||
columnSpacing: isVertical ? 0 : (Style.marginM * scaling)
|
||||
|
||||
// CPU Usage Component
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
Layout.preferredWidth: isVertical ? root.width : iconSize + percentTextWidth + (Style.marginXXS * scaling)
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showCpuUsage
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
Row {
|
||||
id: cpuUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
GridLayout {
|
||||
id: cpuUsageContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
id: cpuUsageIcon
|
||||
text: "speed"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
id: cpuUsageText
|
||||
text: `${SystemStatService.cpuUsage}%`
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
NIcon {
|
||||
icon: "cpu-usage"
|
||||
pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
|
||||
// CPU Temperature Component
|
||||
Row {
|
||||
id: cpuTempLayout
|
||||
// spacing is thin here to compensate for the vertical thermometer icon
|
||||
spacing: Style.marginXXS * scaling
|
||||
NText {
|
||||
text: `${Math.round(SystemStatService.cpuUsage)}%`
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: isVertical ? -1 : percentTextWidth
|
||||
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "thermometer"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// CPU Temperature Component
|
||||
Item {
|
||||
Layout.preferredWidth: isVertical ? root.width : (iconSize + tempTextWidth) + (Style.marginXXS * scaling)
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showCpuTemp
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.cpuTemp}°C`
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
GridLayout {
|
||||
id: cpuTempContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-temperature"
|
||||
pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
|
||||
// Memory Usage Component
|
||||
Row {
|
||||
id: memoryUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
NText {
|
||||
text: `${Math.round(SystemStatService.cpuTemp)}°`
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: isVertical ? -1 : tempTextWidth
|
||||
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "memory"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// Memory Usage Component
|
||||
Item {
|
||||
Layout.preferredWidth: isVertical ? root.width : iconSize + (showMemoryAsPercent ? percentTextWidth : memTextWidth) + (Style.marginXXS * scaling)
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showMemoryUsage
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.memoryUsageGb}G`
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
GridLayout {
|
||||
id: memoryContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "memory"
|
||||
pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: showMemoryAsPercent ? `${Math.round(SystemStatService.memPercent)}%` : `${SystemStatService.memGb.toFixed(1)}G`
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: isVertical ? -1 : (showMemoryAsPercent ? percentTextWidth : memTextWidth)
|
||||
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Download Speed Component
|
||||
Item {
|
||||
Layout.preferredWidth: isVertical ? root.width : iconSize + memTextWidth + (Style.marginXXS * scaling)
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
GridLayout {
|
||||
id: downloadContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "download-speed"
|
||||
pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.rxSpeed) : SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: isVertical ? -1 : memTextWidth
|
||||
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Upload Speed Component
|
||||
Item {
|
||||
Layout.preferredWidth: isVertical ? root.width : iconSize + memTextWidth + (Style.marginXXS * scaling)
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
GridLayout {
|
||||
id: uploadContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "upload-speed"
|
||||
pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.txSpeed) : SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: isVertical ? -1 : memTextWidth
|
||||
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk Usage Component (primary drive)
|
||||
Item {
|
||||
Layout.preferredWidth: isVertical ? root.width : iconSize + percentTextWidth + (Style.marginXXS * scaling)
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showDiskUsage
|
||||
|
||||
GridLayout {
|
||||
id: diskContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "storage"
|
||||
pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.diskPercent}%`
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: isVertical ? -1 : percentTextWidth
|
||||
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
125
Modules/Bar/Widgets/Taskbar.qml
Normal file
@@ -0,0 +1,125 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
readonly property real itemSize: compact ? Style.capsuleHeight * 0.9 * scaling : Style.capsuleHeight * 0.8 * scaling
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// Always visible when there are toplevels
|
||||
implicitWidth: isVerticalBar ? Math.round(Style.capsuleHeight * scaling) : taskbarLayout.implicitWidth + Style.marginM * scaling * 2
|
||||
implicitHeight: isVerticalBar ? taskbarLayout.implicitHeight + Style.marginM * scaling * 2 : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
GridLayout {
|
||||
id: taskbarLayout
|
||||
anchors.fill: parent
|
||||
anchors {
|
||||
leftMargin: isVerticalBar ? undefined : Style.marginM * scaling
|
||||
rightMargin: isVerticalBar ? undefined : Style.marginM * scaling
|
||||
topMargin: compact ? 0 : isVerticalBar ? Style.marginM * scaling : undefined
|
||||
bottomMargin: compact ? 0 : isVerticalBar ? Style.marginM * scaling : undefined
|
||||
}
|
||||
|
||||
// Configure GridLayout to behave like RowLayout or ColumnLayout
|
||||
rows: isVerticalBar ? -1 : 1 // -1 means unlimited
|
||||
columns: isVerticalBar ? 1 : -1 // -1 means unlimited
|
||||
|
||||
rowSpacing: isVerticalBar ? Style.marginXXS * root.scaling : 0
|
||||
columnSpacing: isVerticalBar ? 0 : Style.marginXXS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: CompositorService.windows
|
||||
delegate: Item {
|
||||
id: taskbarItem
|
||||
required property var modelData
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
visible: (!widgetSettings.onlySameOutput || modelData.output == screen.name) && (!widgetSettings.onlyActiveWorkspaces || CompositorService.getActiveWorkspaces().map(ws => ws.id).includes(modelData.workspaceId))
|
||||
|
||||
Layout.preferredWidth: root.itemSize
|
||||
Layout.preferredHeight: root.itemSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
IconImage {
|
||||
|
||||
id: appIcon
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
opacity: modelData.isFocused ? Style.opacityFull : 0.6
|
||||
|
||||
Rectangle {
|
||||
anchors.bottomMargin: -2 * scaling
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
id: iconBackground
|
||||
width: 4 * scaling
|
||||
height: 4 * scaling
|
||||
color: modelData.isFocused ? Color.mPrimary : Color.transparent
|
||||
radius: width * 0.5
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onPressed: function (mouse) {
|
||||
if (!taskbarItem.modelData)
|
||||
return
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
try {
|
||||
CompositorService.focusWindow(taskbarItem.modelData.id)
|
||||
} catch (error) {
|
||||
Logger.error("Taskbar", "Failed to activate toplevel: " + error)
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
try {
|
||||
CompositorService.closeWindow(taskbarItem.modelData.id)
|
||||
} catch (error) {
|
||||
Logger.error("Taskbar", "Failed to close toplevel: " + error)
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: TooltipService.show(taskbarItem, taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown app.", BarService.getTooltipDirection())
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,27 +14,38 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
readonly property real itemSize: 24 * scaling
|
||||
property real scaling: 1.0
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
readonly property real itemSize: isVertical ? width * 0.75 : height * 0.85
|
||||
|
||||
function onLoaded() {
|
||||
// When the widget is fully initialized with its props set the screen for the trayMenu
|
||||
if (trayMenu.item) {
|
||||
trayMenu.item.screen = screen
|
||||
}
|
||||
}
|
||||
|
||||
visible: SystemTray.items.values.length > 0
|
||||
implicitWidth: tray.width + Style.marginM * scaling * 2
|
||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : (trayFlow.implicitWidth + Style.marginS * scaling * 2)
|
||||
implicitHeight: isVertical ? (trayFlow.implicitHeight + Style.marginS * scaling * 2) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Row {
|
||||
id: tray
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Flow {
|
||||
id: trayFlow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginS * scaling
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
width: itemSize
|
||||
height: itemSize
|
||||
@@ -42,6 +53,9 @@ Rectangle {
|
||||
|
||||
IconImage {
|
||||
id: trayIcon
|
||||
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: Style.marginL * scaling
|
||||
height: Style.marginL * scaling
|
||||
@@ -91,7 +105,7 @@ Rectangle {
|
||||
|
||||
modelData.secondaryActivate && modelData.secondaryActivate()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
trayTooltip.hide()
|
||||
TooltipService.hideImmediately()
|
||||
|
||||
// Close the menu if it was visible
|
||||
if (trayPanel && trayPanel.visible) {
|
||||
@@ -102,9 +116,21 @@ Rectangle {
|
||||
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
|
||||
trayPanel.open()
|
||||
|
||||
// Anchor the menu to the tray icon item (parent) and position it below the icon
|
||||
const menuX = (width / 2) - (trayMenu.item.width / 2)
|
||||
const menuY = (Style.barHeight * scaling)
|
||||
// Position menu based on bar position
|
||||
let menuX, menuY
|
||||
if (barPosition === "left") {
|
||||
// For left bar: position menu to the right of the bar
|
||||
menuX = width + Style.marginM * scaling
|
||||
menuY = 0
|
||||
} else if (barPosition === "right") {
|
||||
// For right bar: position menu to the left of the bar
|
||||
menuX = -trayMenu.item.width - Style.marginM * scaling
|
||||
menuY = 0
|
||||
} else {
|
||||
// For horizontal bars: center horizontally and position below
|
||||
menuX = (width / 2) - (trayMenu.item.width / 2)
|
||||
menuY = Math.round(Style.barHeight * scaling)
|
||||
}
|
||||
trayMenu.item.menu = modelData.menu
|
||||
trayMenu.item.showAt(parent, menuX, menuY)
|
||||
} else {
|
||||
@@ -112,15 +138,11 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: trayTooltip.show()
|
||||
onExited: trayTooltip.hide()
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: trayTooltip
|
||||
target: trayIcon
|
||||
text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
onEntered: {
|
||||
trayPanel.close()
|
||||
TooltipService.show(trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection())
|
||||
}
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,13 +160,14 @@ Rectangle {
|
||||
|
||||
function open() {
|
||||
visible = true
|
||||
|
||||
PanelService.willOpenPanel(trayPanel)
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false
|
||||
trayMenu.item.hideMenu()
|
||||
if (trayMenu.item) {
|
||||
trayMenu.item.hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking outside of the rectangle to close
|
||||
|
||||
@@ -1,28 +1,51 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstVolumeReceived: false
|
||||
property int wheelAccumulator: 0
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
function getIcon() {
|
||||
if (AudioService.muted) {
|
||||
return "volume_off"
|
||||
return "volume-mute"
|
||||
}
|
||||
return AudioService.volume <= Number.EPSILON ? "volume_off" : (AudioService.volume < 0.33 ? "volume_down" : "volume_up")
|
||||
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
|
||||
}
|
||||
|
||||
// Connection used to open the pill when volume changes
|
||||
@@ -49,27 +72,42 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
NPill {
|
||||
BarPill {
|
||||
id: pill
|
||||
icon: getIcon()
|
||||
iconCircleColor: Color.mPrimary
|
||||
collapsedIconColor: Color.mOnSurface
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.floor(AudioService.volume * 100) + "%"
|
||||
tooltipText: "Volume: " + Math.round(
|
||||
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
|
||||
|
||||
onWheel: function (angle) {
|
||||
if (angle > 0) {
|
||||
screen: root.screen
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: getIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.round(AudioService.volume * 100)
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: I18n.tr("tooltips.volume-at", {
|
||||
"volume": Math.round(AudioService.volume * 100)
|
||||
})
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
if (wheelAccumulator >= 120) {
|
||||
wheelAccumulator = 0
|
||||
AudioService.increaseVolume()
|
||||
} else if (angle < 0) {
|
||||
} else if (wheelAccumulator <= -120) {
|
||||
wheelAccumulator = 0
|
||||
AudioService.decreaseVolume()
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
AudioService.setOutputMuted(!AudioService.muted)
|
||||
}
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
||||
settingsPanel.open(screen)
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
|
||||
settingsPanel.open()
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["pwvucontrol"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
Modules/Bar/Widgets/WallpaperSelector.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: "wallpaper-selector"
|
||||
tooltipText: I18n.tr("tooltips.open-wallpaper-selector")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
|
||||
}
|
||||
@@ -11,29 +11,21 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
visible: Settings.data.network.wifiEnabled
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("WiFi", "Widget component completed")
|
||||
Logger.log("WiFi", "NetworkService available:", !!NetworkService)
|
||||
if (NetworkService) {
|
||||
Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks)
|
||||
}
|
||||
}
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
tooltipText: I18n.tr("tooltips.manage-wifi")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
icon: {
|
||||
try {
|
||||
if (NetworkService.ethernet)
|
||||
return "lan"
|
||||
if (NetworkService.ethernetConnected) {
|
||||
return "ethernet"
|
||||
}
|
||||
let connected = false
|
||||
let signalStrength = 0
|
||||
for (const net in NetworkService.networks) {
|
||||
@@ -43,19 +35,12 @@ NIconButton {
|
||||
break
|
||||
}
|
||||
}
|
||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
|
||||
} catch (error) {
|
||||
Logger.error("WiFi", "Error getting icon:", error)
|
||||
Logger.error("Wi-Fi", "Error getting icon:", error)
|
||||
return "signal_wifi_bad"
|
||||
}
|
||||
}
|
||||
tooltipText: "Network / WiFi"
|
||||
onClicked: {
|
||||
try {
|
||||
Logger.log("WiFi", "Button clicked, toggling panel")
|
||||
PanelService.getPanel("wifiPanel")?.toggle(screen)
|
||||
} catch (error) {
|
||||
Logger.error("WiFi", "Error toggling panel:", error)
|
||||
}
|
||||
}
|
||||
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,44 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen: null
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
readonly property real baseDimensionRatio: {
|
||||
const b = compact ? 0.85 : 0.65
|
||||
if (widgetSettings.labelMode === "none") {
|
||||
return b * 0.75
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
|
||||
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
|
||||
|
||||
property bool isDestroying: false
|
||||
property bool hovered: false
|
||||
@@ -22,26 +54,72 @@ Item {
|
||||
property bool effectsActive: false
|
||||
property color effectColor: Color.mPrimary
|
||||
|
||||
property int horizontalPadding: Math.round(16 * scaling)
|
||||
property int spacingBetweenPills: Math.round(8 * scaling)
|
||||
property int horizontalPadding: Math.round(Style.marginS * scaling)
|
||||
property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
|
||||
|
||||
// Wheel scroll handling
|
||||
property int wheelAccumulatedDelta: 0
|
||||
property bool wheelCooldown: false
|
||||
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
|
||||
implicitHeight: Math.round(36 * scaling)
|
||||
implicitWidth: {
|
||||
implicitWidth: isVertical ? Math.round(Style.barHeight * scaling) : computeWidth()
|
||||
implicitHeight: isVertical ? computeHeight() : Math.round(Style.barHeight * scaling)
|
||||
|
||||
function getWorkspaceWidth(ws) {
|
||||
const d = Style.capsuleHeight * root.baseDimensionRatio
|
||||
const factor = ws.isFocused ? 2.2 : 1
|
||||
return d * factor * scaling
|
||||
}
|
||||
|
||||
function getWorkspaceHeight(ws) {
|
||||
const d = Style.capsuleHeight * root.baseDimensionRatio
|
||||
const factor = ws.isFocused ? 2.2 : 1
|
||||
return d * factor * scaling
|
||||
}
|
||||
|
||||
function computeWidth() {
|
||||
let total = 0
|
||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i)
|
||||
if (ws.isFocused)
|
||||
total += Math.round(44 * scaling)
|
||||
else if (ws.isActive)
|
||||
total += Math.round(28 * scaling)
|
||||
else
|
||||
total += Math.round(16 * scaling)
|
||||
total += getWorkspaceWidth(ws)
|
||||
}
|
||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
||||
total += horizontalPadding * 2
|
||||
return total
|
||||
return Math.round(total)
|
||||
}
|
||||
|
||||
function computeHeight() {
|
||||
let total = 0
|
||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i)
|
||||
total += getWorkspaceHeight(ws)
|
||||
}
|
||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
||||
total += horizontalPadding * 2
|
||||
return Math.round(total)
|
||||
}
|
||||
|
||||
function getFocusedLocalIndex() {
|
||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||
if (localWorkspaces.get(i).isFocused === true)
|
||||
return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function switchByOffset(offset) {
|
||||
if (localWorkspaces.count === 0)
|
||||
return
|
||||
var current = getFocusedLocalIndex()
|
||||
if (current < 0)
|
||||
current = 0
|
||||
var next = (current + offset) % localWorkspaces.count
|
||||
if (next < 0)
|
||||
next = localWorkspaces.count - 1
|
||||
const ws = localWorkspaces.get(next)
|
||||
if (ws && ws.idx !== undefined)
|
||||
CompositorService.switchToWorkspace(ws.idx)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -53,9 +131,10 @@ Item {
|
||||
}
|
||||
|
||||
onScreenChanged: refreshWorkspaces()
|
||||
onHideUnoccupiedChanged: refreshWorkspaces()
|
||||
|
||||
Connections {
|
||||
target: WorkspaceService
|
||||
target: CompositorService
|
||||
function onWorkspacesChanged() {
|
||||
refreshWorkspaces()
|
||||
}
|
||||
@@ -64,14 +143,18 @@ Item {
|
||||
function refreshWorkspaces() {
|
||||
localWorkspaces.clear()
|
||||
if (screen !== null) {
|
||||
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
||||
const ws = WorkspaceService.workspaces.get(i)
|
||||
for (var i = 0; i < CompositorService.workspaces.count; i++) {
|
||||
const ws = CompositorService.workspaces.get(i)
|
||||
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
||||
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) {
|
||||
continue
|
||||
}
|
||||
localWorkspaces.append(ws)
|
||||
}
|
||||
}
|
||||
}
|
||||
workspaceRepeater.model = localWorkspaces
|
||||
workspaceRepeaterHorizontal.model = localWorkspaces
|
||||
workspaceRepeaterVertical.model = localWorkspaces
|
||||
updateWorkspaceFocus()
|
||||
}
|
||||
|
||||
@@ -103,7 +186,7 @@ Item {
|
||||
property: "masterProgress"
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
duration: 1000
|
||||
duration: Style.animationSlow * 2
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
PropertyAction {
|
||||
@@ -120,54 +203,108 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: workspaceBackground
|
||||
width: parent.width - Style.marginS * scaling * 2
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
width: isVertical ? Math.round(Style.capsuleHeight * scaling) : parent.width
|
||||
height: isVertical ? parent.height : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowColor: Color.mShadow
|
||||
shadowVerticalOffset: 0
|
||||
shadowHorizontalOffset: 0
|
||||
shadowOpacity: 0.10
|
||||
}
|
||||
|
||||
// Debounce timer for wheel interactions
|
||||
Timer {
|
||||
id: wheelDebounce
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.wheelCooldown = false
|
||||
root.wheelAccumulatedDelta = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to switch workspaces
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
target: root
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: function (event) {
|
||||
if (root.wheelCooldown)
|
||||
return
|
||||
// Prefer vertical delta, fall back to horizontal if needed
|
||||
var dy = event.angleDelta.y
|
||||
var dx = event.angleDelta.x
|
||||
var useDy = Math.abs(dy) >= Math.abs(dx)
|
||||
var delta = useDy ? dy : dx
|
||||
// One notch is typically 120
|
||||
root.wheelAccumulatedDelta += delta
|
||||
var step = 120
|
||||
if (Math.abs(root.wheelAccumulatedDelta) >= step) {
|
||||
var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1
|
||||
// For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign)
|
||||
// For horizontal layout, same mapping using vertical wheel
|
||||
root.switchByOffset(direction)
|
||||
root.wheelCooldown = true
|
||||
wheelDebounce.restart()
|
||||
root.wheelAccumulatedDelta = 0
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
Row {
|
||||
id: pillRow
|
||||
spacing: spacingBetweenPills
|
||||
anchors.verticalCenter: workspaceBackground.verticalCenter
|
||||
width: root.width - horizontalPadding * 2
|
||||
x: horizontalPadding
|
||||
visible: !isVertical
|
||||
|
||||
Repeater {
|
||||
id: workspaceRepeater
|
||||
id: workspaceRepeaterHorizontal
|
||||
model: localWorkspaces
|
||||
Item {
|
||||
id: workspacePillContainer
|
||||
height: Math.round(12 * scaling)
|
||||
width: {
|
||||
if (model.isFocused)
|
||||
return Math.round(44 * scaling)
|
||||
else if (model.isActive)
|
||||
return Math.round(28 * scaling)
|
||||
else
|
||||
return Math.round(16 * scaling)
|
||||
}
|
||||
width: root.getWorkspaceWidth(model)
|
||||
height: Style.capsuleHeight * root.baseDimensionRatio * scaling
|
||||
|
||||
Rectangle {
|
||||
id: workspacePill
|
||||
id: pill
|
||||
anchors.fill: parent
|
||||
radius: {
|
||||
if (model.isFocused)
|
||||
return Math.round(12 * scaling)
|
||||
else
|
||||
// half of focused height (if you want to animate this too)
|
||||
return Math.round(6 * scaling)
|
||||
|
||||
Loader {
|
||||
active: (labelMode !== "none")
|
||||
sourceComponent: Component {
|
||||
NText {
|
||||
x: (pill.width - width) / 2
|
||||
y: (pill.height - height) / 2 + (height - contentHeight) / 2
|
||||
text: {
|
||||
if (labelMode === "name" && model.name && model.name.length > 0) {
|
||||
return model.name.substring(0, 2)
|
||||
} else {
|
||||
return model.idx.toString()
|
||||
}
|
||||
}
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: model.isFocused ? workspacePillContainer.height * 0.45 : workspacePillContainer.height * 0.42
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.weight: Style.fontWeightBold
|
||||
wrapMode: Text.Wrap
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Color.mOnPrimary
|
||||
if (model.isUrgent)
|
||||
return Color.mOnError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mOnSecondary
|
||||
|
||||
return Color.mOnSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
radius: width * 0.5
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Color.mPrimary
|
||||
@@ -175,10 +312,8 @@ Item {
|
||||
return Color.mError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mSecondary
|
||||
if (model.isUrgent)
|
||||
return Color.mError
|
||||
|
||||
return Color.mOutline
|
||||
return Qt.alpha(Color.mSecondary, 0.3)
|
||||
}
|
||||
scale: model.isFocused ? 1.0 : 0.9
|
||||
z: 0
|
||||
@@ -188,7 +323,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WorkspaceService.switchToWorkspace(model.idx)
|
||||
CompositorService.switchToWorkspace(model.idx)
|
||||
}
|
||||
hoverEnabled: true
|
||||
}
|
||||
@@ -260,4 +395,148 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars
|
||||
Column {
|
||||
id: pillColumn
|
||||
spacing: spacingBetweenPills
|
||||
anchors.horizontalCenter: workspaceBackground.horizontalCenter
|
||||
y: horizontalPadding
|
||||
visible: isVertical
|
||||
|
||||
Repeater {
|
||||
id: workspaceRepeaterVertical
|
||||
model: localWorkspaces
|
||||
Item {
|
||||
id: workspacePillContainerVertical
|
||||
width: Style.capsuleHeight * root.baseDimensionRatio * scaling
|
||||
height: root.getWorkspaceHeight(model)
|
||||
|
||||
Rectangle {
|
||||
id: pillVertical
|
||||
anchors.fill: parent
|
||||
|
||||
Loader {
|
||||
active: (labelMode !== "none")
|
||||
sourceComponent: Component {
|
||||
NText {
|
||||
x: (pillVertical.width - width) / 2
|
||||
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
|
||||
text: {
|
||||
if (labelMode === "name" && model.name && model.name.length > 0) {
|
||||
return model.name.substring(0, 2)
|
||||
} else {
|
||||
return model.idx.toString()
|
||||
}
|
||||
}
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: model.isFocused ? workspacePillContainerVertical.width * 0.45 : workspacePillContainerVertical.width * 0.42
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.weight: Style.fontWeightBold
|
||||
wrapMode: Text.Wrap
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Color.mOnPrimary
|
||||
if (model.isUrgent)
|
||||
return Color.mOnError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mOnSecondary
|
||||
|
||||
return Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
radius: width * 0.5
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Color.mPrimary
|
||||
if (model.isUrgent)
|
||||
return Color.mError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mSecondary
|
||||
|
||||
return Color.mOutline
|
||||
}
|
||||
scale: model.isFocused ? 1.0 : 0.9
|
||||
z: 0
|
||||
|
||||
MouseArea {
|
||||
id: pillMouseAreaVertical
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
CompositorService.switchToWorkspace(model.idx)
|
||||
}
|
||||
hoverEnabled: true
|
||||
}
|
||||
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
// Burst effect overlay for focused pill (smaller outline)
|
||||
Rectangle {
|
||||
id: pillBurstVertical
|
||||
anchors.centerIn: workspacePillContainerVertical
|
||||
width: workspacePillContainerVertical.width + 18 * root.masterProgress * scale
|
||||
height: workspacePillContainerVertical.height + 18 * root.masterProgress * scale
|
||||
radius: width / 2
|
||||
color: Color.transparent
|
||||
border.color: root.effectColor
|
||||
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling))
|
||||
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
|
||||
visible: root.effectsActive && model.isFocused
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||