mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
Compare commits
517 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 | ||
|
|
cdfb110007 | ||
|
|
b7d8f92414 |
110
.github/workflows/update-aur-package.yml
vendored
Normal file
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"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.qmlls.ini
|
||||
|
||||
34
Assets/ColorScheme/Ayu.json
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"
|
||||
}
|
||||
}
|
||||
34
Assets/ColorScheme/Kanagawa.json
Normal file
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"
|
||||
}
|
||||
}
|
||||
@@ -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
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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -15,7 +15,7 @@ Singleton {
|
||||
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"')
|
||||
@@ -25,11 +25,13 @@ Singleton {
|
||||
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]")
|
||||
@@ -51,7 +53,7 @@ Singleton {
|
||||
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\"")
|
||||
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]")
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
palette = 0={{colors.surface.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.on_background.default.hex}}
|
||||
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}}
|
||||
|
||||
cursor-color = {{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}}
|
||||
foreground = {{colors.on_surface.default.hex}}
|
||||
background = {{colors.surface.default.hex}}
|
||||
selection-foreground = {{colors.on_secondary.default.hex}}
|
||||
selection-background = {{colors.secondary_fixed_dim.default.hex}}
|
||||
|
||||
selection-background = {{colors.on_secondary.default.hex}}
|
||||
selection-foreground = {{colors.secondary_fixed_dim.default.hex}}
|
||||
|
||||
1433
Assets/Translations/de.json
Normal file
1433
Assets/Translations/de.json
Normal file
File diff suppressed because it is too large
Load Diff
1433
Assets/Translations/en.json
Normal file
1433
Assets/Translations/en.json
Normal file
File diff suppressed because it is too large
Load Diff
1429
Assets/Translations/es.json
Normal file
1429
Assets/Translations/es.json
Normal file
File diff suppressed because it is too large
Load Diff
1429
Assets/Translations/fr.json
Normal file
1429
Assets/Translations/fr.json
Normal file
File diff suppressed because it is too large
Load Diff
1429
Assets/Translations/pt.json
Normal file
1429
Assets/Translations/pt.json
Normal file
File diff suppressed because it is too large
Load Diff
1429
Assets/Translations/zh-CN.json
Normal file
1429
Assets/Translations/zh-CN.json
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 760 KiB |
194
Assets/settings-default.json
Normal file
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
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
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
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
|
||||
@@ -1,32 +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!"
|
||||
|
||||
# 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/debian-logo.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
|
||||
365
Commons/I18n.qml
Normal file
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)
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,41 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Commons.IconsSets
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Expose the font family name for easy access
|
||||
readonly property string fontFamily: fontLoader.name
|
||||
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"
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
function get(iconName) {
|
||||
// Check in aliases first
|
||||
if (aliases[iconName] !== undefined) {
|
||||
@@ -30,20 +49,37 @@ Singleton {
|
||||
return icons[iconName]
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: fontLoader
|
||||
source: Quickshell.shellDir + fontPath
|
||||
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 + ")")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Monitor font loading status
|
||||
Connections {
|
||||
target: fontLoader
|
||||
function onStatusChanged() {
|
||||
if (fontLoader.status === FontLoader.Ready) {
|
||||
Logger.log("Icons", "Font loaded successfully:", fontFamily)
|
||||
} else if (fontLoader.status === FontLoader.Error) {
|
||||
Logger.error("Icons", "Font failed to load")
|
||||
}
|
||||
}
|
||||
function reloadFont() {
|
||||
Logger.log("Icons", "Forcing font reload...")
|
||||
fontVersion++
|
||||
loadFontWithCacheBusting()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,16 @@ 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
|
||||
@@ -16,24 +22,351 @@ Singleton {
|
||||
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 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 defaultLocation: "Tokyo"
|
||||
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
|
||||
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
|
||||
|
||||
// Used to access via Settings.data.xxx.yyy
|
||||
readonly property alias data: adapter
|
||||
|
||||
property bool isLoaded: false
|
||||
property bool directoriesCreated: false
|
||||
|
||||
// Signal emitted when settings are loaded after startupcale changes
|
||||
signal settingsLoaded
|
||||
|
||||
// -----------------------------------------------------
|
||||
// -----------------------------------------------------
|
||||
// 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() {
|
||||
@@ -71,25 +404,42 @@ Singleton {
|
||||
// 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
|
||||
}
|
||||
|
||||
const sections = ["left", "center", "right"]
|
||||
|
||||
// -----------------
|
||||
// 1st. check our settings are not super old, when we only had the widget type as a plain string
|
||||
// 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]
|
||||
if (typeof widget === "string") {
|
||||
adapter.bar.widgets[sectionName][i] = {
|
||||
"id": widget
|
||||
}
|
||||
|
||||
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]
|
||||
@@ -97,14 +447,15 @@ Singleton {
|
||||
for (var i = widgets.length - 1; i >= 0; i--) {
|
||||
var widget = widgets[i]
|
||||
if (!BarWidgetRegistry.hasWidget(widget.id)) {
|
||||
widgets.splice(i, 1)
|
||||
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
|
||||
widgets.splice(i, 1)
|
||||
removedWidget = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 3nd. migrate global settings to user settings
|
||||
// 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++) {
|
||||
@@ -122,10 +473,29 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade the density of the bar so the look stay the same for people who upgrade.
|
||||
if (adapter.settingsVersion == 2) {
|
||||
adapter.bar.density = "comfortable"
|
||||
adapter.settingsVersion++
|
||||
// -----------------
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,42 +504,20 @@ Singleton {
|
||||
// Backup the widget definition before altering
|
||||
const widgetBefore = JSON.stringify(widget)
|
||||
|
||||
// Migrate old bar settings to proper per widget settings
|
||||
switch (widget.id) {
|
||||
case "ActiveWindow":
|
||||
widget.showIcon = widget.showIcon !== undefined ? widget.showIcon : adapter.bar.showActiveWindowIcon
|
||||
break
|
||||
case "Battery":
|
||||
widget.alwaysShowPercentage = widget.alwaysShowPercentage !== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage
|
||||
break
|
||||
case "Clock":
|
||||
widget.use12HourClock = widget.use12HourClock !== undefined ? widget.use12HourClock : adapter.location.use12HourClock
|
||||
widget.reverseDayMonth = widget.reverseDayMonth !== undefined ? widget.reverseDayMonth : adapter.location.reverseDayMonth
|
||||
if (widget.showDate !== undefined) {
|
||||
widget.displayFormat = "time-date"
|
||||
} else if (widget.showSeconds) {
|
||||
widget.displayFormat = "time-seconds"
|
||||
// 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]
|
||||
}
|
||||
delete widget.showDate
|
||||
delete widget.showSeconds
|
||||
break
|
||||
case "MediaMini":
|
||||
widget.showAlbumArt = widget.showAlbumArt !== undefined ? widget.showAlbumArt : adapter.audio.showMiniplayerAlbumArt
|
||||
widget.showVisualizer = widget.showVisualizer !== undefined ? widget.showVisualizer : adapter.audio.showMiniplayerCava
|
||||
break
|
||||
case "SidePanelToggle":
|
||||
widget.useDistroLogo = widget.useDistroLogo !== undefined ? widget.useDistroLogo : adapter.bar.useDistroLogo
|
||||
break
|
||||
case "SystemMonitor":
|
||||
widget.showNetworkStats = widget.showNetworkStats !== undefined ? widget.showNetworkStats : adapter.bar.showNetworkStats
|
||||
break
|
||||
case "Workspace":
|
||||
widget.labelMode = widget.labelMode !== undefined ? widget.labelMode : adapter.bar.showWorkspaceLabel
|
||||
break
|
||||
}
|
||||
|
||||
// Inject missing default setting (metaData) from BarWidgetRegistry
|
||||
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
const k = keys[i]
|
||||
if (k === "id" || k === "allowUserSettings") {
|
||||
@@ -185,300 +533,4 @@ Singleton {
|
||||
const widgetAfter = JSON.stringify(widget)
|
||||
return (widgetAfter !== widgetBefore)
|
||||
}
|
||||
// -----------------------------------------------------
|
||||
// Kickoff essential services
|
||||
function kickOffServices() {
|
||||
// Ensure our location singleton is created as soon as possible so we start fetching weather asap
|
||||
LocationService.init()
|
||||
|
||||
NightLightService.apply()
|
||||
|
||||
ColorSchemeService.init()
|
||||
|
||||
MatugenService.init()
|
||||
|
||||
FontService.init()
|
||||
|
||||
HooksService.init()
|
||||
|
||||
BluetoothService.init()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 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", cacheDirImages])
|
||||
|
||||
// Mark directories as created and trigger file loading
|
||||
directoriesCreated = true
|
||||
}
|
||||
|
||||
// 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: 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", "----------------------------")
|
||||
Logger.log("Settings", "Settings loaded successfully")
|
||||
|
||||
upgradeSettingsData()
|
||||
|
||||
validateMonitorConfigurations()
|
||||
|
||||
kickOffServices()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property int settingsVersion: 3
|
||||
|
||||
// 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
|
||||
|
||||
property bool showActiveWindowIcon: true // TODO: delete
|
||||
property bool alwaysShowBatteryPercentage: false // TODO: delete
|
||||
property bool showNetworkStats: false // TODO: delete
|
||||
property bool useDistroLogo: false // TODO: delete
|
||||
property string showWorkspaceLabel: "none" // TODO: delete
|
||||
|
||||
// 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": "ScreenRecorderIndicator"
|
||||
}, {
|
||||
"id": "Tray"
|
||||
}, {
|
||||
"id": "NotificationHistory"
|
||||
}, {
|
||||
"id": "WiFi"
|
||||
}, {
|
||||
"id": "Bluetooth"
|
||||
}, {
|
||||
"id": "Battery"
|
||||
}, {
|
||||
"id": "Volume"
|
||||
}, {
|
||||
"id": "Brightness"
|
||||
}, {
|
||||
"id": "NightLight"
|
||||
}, {
|
||||
"id": "Clock"
|
||||
}, {
|
||||
"id": "SidePanelToggle"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// general
|
||||
property JsonObject general: JsonObject {
|
||||
property string avatarImage: defaultAvatar
|
||||
property bool dimDesktop: true
|
||||
property bool showScreenCorners: false
|
||||
property bool forceBlackScreenCorners: false
|
||||
property real radiusRatio: 1.0
|
||||
property real screenRadiusRatio: 1.0
|
||||
// Animation speed multiplier (0.1x - 2.0x)
|
||||
property real animationSpeed: 1.0
|
||||
}
|
||||
|
||||
// location
|
||||
property JsonObject location: JsonObject {
|
||||
property string name: defaultLocation
|
||||
property bool useFahrenheit: false
|
||||
|
||||
property bool reverseDayMonth: false // TODO: delete
|
||||
property bool use12HourClock: false // TODO: delete
|
||||
property bool showDateWithClock: false // TODO: delete
|
||||
}
|
||||
|
||||
// screen recorder
|
||||
property JsonObject screenRecorder: JsonObject {
|
||||
property string directory: defaultVideosDirectory
|
||||
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: defaultWallpapersDirectory
|
||||
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 {
|
||||
// When disabled, Launcher hides clipboard command and ignores cliphist
|
||||
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
|
||||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property real backgroundOpacity: 1.0
|
||||
property real floatingRatio: 1.0
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
// network
|
||||
property JsonObject network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
property bool bluetoothEnabled: true
|
||||
}
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool doNotDisturb: false
|
||||
property list<string> monitors: []
|
||||
// Last time the user opened the notification history (ms since epoch)
|
||||
property real lastSeenTs: 0
|
||||
// Duration settings for different urgency levels (in seconds)
|
||||
property int lowUrgencyDuration: 3
|
||||
property int normalUrgencyDuration: 8
|
||||
property int criticalUrgencyDuration: 15
|
||||
}
|
||||
|
||||
// audio
|
||||
property JsonObject audio: JsonObject {
|
||||
property int volumeStep: 5
|
||||
property int cavaFrameRate: 60
|
||||
property string visualizerType: "linear"
|
||||
property list<string> mprisBlacklist: []
|
||||
property string preferredPlayer: ""
|
||||
|
||||
property bool showMiniplayerAlbumArt: false // TODO: delete
|
||||
property bool showMiniplayerCava: false // TODO: delete
|
||||
}
|
||||
|
||||
// ui
|
||||
property JsonObject ui: JsonObject {
|
||||
property string fontDefault: "Roboto"
|
||||
property string fontFixed: "DejaVu Sans Mono"
|
||||
property string fontBillboard: "Inter"
|
||||
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: ""
|
||||
property bool darkMode: true
|
||||
}
|
||||
|
||||
// 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: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ Singleton {
|
||||
property real opacityFull: 1.0
|
||||
|
||||
// Animation duration (ms)
|
||||
property int animationFast: Math.round(150 / Settings.data.general.animationSpeed)
|
||||
property int animationNormal: Math.round(300 / Settings.data.general.animationSpeed)
|
||||
property int animationSlow: Math.round(450 / Settings.data.general.animationSpeed)
|
||||
property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
|
||||
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
|
||||
|
||||
@@ -26,6 +26,7 @@ Singleton {
|
||||
"search": "search",
|
||||
"warning": "exclamation-circle",
|
||||
"stop": "player-stop-filled",
|
||||
"busy": "hourglass-empty",
|
||||
"media-pause": "player-pause-filled",
|
||||
"media-play": "player-play-filled",
|
||||
"media-prev": "player-skip-back-filled",
|
||||
@@ -44,6 +45,7 @@ Singleton {
|
||||
"keyboard": "keyboard",
|
||||
"shutdown": "power",
|
||||
"lock": "lock",
|
||||
"lock-pause": "lock-pause",
|
||||
"logout": "logout",
|
||||
"reboot": "refresh",
|
||||
"suspend": "player-pause",
|
||||
@@ -55,6 +57,9 @@ Singleton {
|
||||
"keep-awake-on": "mug",
|
||||
"keep-awake-off": "mug-off",
|
||||
"disc": "disc-filled",
|
||||
"eye": "eye",
|
||||
"pin": "pin",
|
||||
"unpin": "pinned-off",
|
||||
"image": "photo",
|
||||
"dark-mode": "contrast-filled",
|
||||
"camera-video": "video",
|
||||
@@ -66,6 +71,8 @@ Singleton {
|
||||
"chevron-down": "chevron-down",
|
||||
"caret-up": "caret-up-filled",
|
||||
"caret-down": "caret-down-filled",
|
||||
"star": "star",
|
||||
"star-off": "star-off",
|
||||
"battery-exclamation": "battery-exclamation",
|
||||
"battery-charging": "battery-charging",
|
||||
"battery-4": "battery-4",
|
||||
@@ -101,13 +108,14 @@ Singleton {
|
||||
"settings-display": "device-desktop",
|
||||
"settings-network": "sitemap",
|
||||
"settings-brightness": "brightness-up",
|
||||
"settings-weather": "cloud-sun",
|
||||
"settings-location": "world-pin",
|
||||
"settings-color-scheme": "palette",
|
||||
"settings-wallpaper": "paint",
|
||||
"settings-wallpaper-selector": "library-photo",
|
||||
"settings-screen-recorder": "video",
|
||||
"settings-hooks": "link",
|
||||
"settings-notification": "bell",
|
||||
"settings-notifications": "bell",
|
||||
"settings-osd": "picture-in-picture",
|
||||
"settings-about": "info-square-rounded",
|
||||
"bluetooth": "bluetooth",
|
||||
"bt-device-generic": "bluetooth",
|
||||
@@ -118,7 +126,33 @@ Singleton {
|
||||
"bt-device-watch": "device-watch",
|
||||
"bt-device-speaker": "device-speaker",
|
||||
"bt-device-tv": "device-tv",
|
||||
"noctalia": "noctalia"
|
||||
"noctalia": "noctalia",
|
||||
"hyprland": "hyprland",
|
||||
"filepicker-folder": "folder",
|
||||
"filepicker-refresh": "refresh",
|
||||
"filepicker-close": "x",
|
||||
"filepicker-arrow-left": "arrow-left",
|
||||
"filepicker-arrow-up": "arrow-up",
|
||||
"filepicker-home": "home",
|
||||
"filepicker-layout-grid": "layout-grid",
|
||||
"filepicker-list": "list",
|
||||
"filepicker-search": "search",
|
||||
"filepicker-x": "x",
|
||||
"filepicker-photo": "photo",
|
||||
"filepicker-check": "check",
|
||||
"filepicker-file-text": "file-text",
|
||||
"filepicker-video": "video",
|
||||
"filepicker-music": "music",
|
||||
"filepicker-archive": "archive",
|
||||
"filepicker-table": "table",
|
||||
"filepicker-presentation": "presentation",
|
||||
"filepicker-code": "code",
|
||||
"filepicker-settings": "settings",
|
||||
"filepicker-file": "file",
|
||||
"filepicker-text": "file-text",
|
||||
"filepicker-eye": "eye",
|
||||
"filepicker-eye-off": "eye-off",
|
||||
"filepicker-folder-current": "checks"
|
||||
}
|
||||
|
||||
// Fonts Codepoints - do not change!
|
||||
@@ -3470,6 +3504,7 @@ Singleton {
|
||||
"http-que-off": "\u{100df}",
|
||||
"http-trace": "\u{fa30}",
|
||||
"http-trace-off": "\u{100de}",
|
||||
"hyprland": "\u{ec6a}",
|
||||
"ice-cream": "\u{eac2}",
|
||||
"ice-cream-2": "\u{ee9f}",
|
||||
"ice-cream-off": "\u{f148}",
|
||||
@@ -4302,6 +4337,7 @@ Singleton {
|
||||
"news-off": "\u{f167}",
|
||||
"nfc": "\u{eeb7}",
|
||||
"nfc-off": "\u{f168}",
|
||||
"niri": "\u{ec32}",
|
||||
"noctalia": "\u{ec33}",
|
||||
"no-copyright": "\u{efb9}",
|
||||
"no-creative-commons": "\u{efba}",
|
||||
132
Commons/Time.qml
132
Commons/Time.qml
@@ -8,6 +8,7 @@ import qs.Services
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Current date
|
||||
property var date: new Date()
|
||||
|
||||
// Returns a Unix Timestamp (in seconds)
|
||||
@@ -15,90 +16,73 @@ Singleton {
|
||||
return Math.floor(date / 1000)
|
||||
}
|
||||
|
||||
function formatDate(reverseDayMonth = true) {
|
||||
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}, ` + (reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.date = new Date()
|
||||
}
|
||||
|
||||
// Formats a Date object into a YYYYMMDD-HHMMSS string.
|
||||
function getFormattedTimestamp(date) {
|
||||
if (!date) {
|
||||
date = new Date()
|
||||
}
|
||||
const year = date.getFullYear()
|
||||
|
||||
/**
|
||||
* 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')
|
||||
|
||||
// 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')
|
||||
|
||||
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'
|
||||
return `${year}${month}${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
// Floor the input to handle decimal seconds
|
||||
totalSeconds = Math.floor(totalSeconds)
|
||||
// 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'
|
||||
}
|
||||
|
||||
const days = Math.floor(totalSeconds / 86400)
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
// Floor the input to handle decimal seconds
|
||||
totalSeconds = Math.floor(totalSeconds)
|
||||
|
||||
const parts = []
|
||||
if (days)
|
||||
parts.push(`${days}d`)
|
||||
if (hours)
|
||||
parts.push(`${hours}h`)
|
||||
if (minutes)
|
||||
parts.push(`${minutes}m`)
|
||||
const days = Math.floor(totalSeconds / 86400)
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
|
||||
// Only show seconds if no hours and no minutes
|
||||
if (!hours && !minutes) {
|
||||
parts.push(`${seconds}s`)
|
||||
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('')
|
||||
}
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
|
||||
onTriggered: root.date = new Date()
|
||||
}
|
||||
// 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
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;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Widgets
|
||||
|
||||
Variants {
|
||||
@@ -45,14 +44,27 @@ Variants {
|
||||
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 assign wallpaper immediately
|
||||
Component.onCompleted: {
|
||||
fillMode = WallpaperService.getFillModeUniform()
|
||||
// On startup, defer assigning wallpaper until the service cache is ready
|
||||
function _startWallpaperOnceReady() {
|
||||
if (!modelData) {
|
||||
Qt.callLater(_startWallpaperOnceReady)
|
||||
return
|
||||
}
|
||||
|
||||
var path = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
|
||||
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() {
|
||||
@@ -231,6 +243,9 @@ Variants {
|
||||
easing.type: Easing.InOutCubic
|
||||
onFinished: {
|
||||
// Swap images after transition completes
|
||||
if (currentWallpaper.source !== "") {
|
||||
currentWallpaper.source = ""
|
||||
}
|
||||
currentWallpaper.source = nextWallpaper.source
|
||||
nextWallpaper.source = ""
|
||||
transitionProgress = 0.0
|
||||
@@ -243,6 +258,9 @@ Variants {
|
||||
function setWallpaperImmediate(source) {
|
||||
transitionAnimation.stop()
|
||||
transitionProgress = 0.0
|
||||
if (currentWallpaper.source !== "") {
|
||||
currentWallpaper.source = ""
|
||||
}
|
||||
currentWallpaper.source = source
|
||||
nextWallpaper.source = ""
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Loader {
|
||||
required property ShellScreen modelData
|
||||
|
||||
// Dimmer is only active on the screen where the panel is currently open.
|
||||
active: {
|
||||
if (Settings.isLoaded && Settings.data.general.dimDesktop && modelData !== undefined && PanelService.openedPanel !== null && PanelService.openedPanel.item !== undefined && PanelService.openedPanel.item !== null) {
|
||||
return (PanelService.openedPanel.item.screen === modelData)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: panel
|
||||
|
||||
property real customOpacity: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
if (modelData) {
|
||||
Logger.log("Dimmer", "Loaded on", modelData.name)
|
||||
}
|
||||
|
||||
// When a NPanel opens it seems it is initialized with the primary screen for a very brief moment
|
||||
// before the screen actually updates to the proper value. We use a timer to delay the fade in to avoid
|
||||
// a single frame flicker on the main screen when opening a panel on another screen.
|
||||
fadeInTimer.start()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PanelService
|
||||
function onWillClose() {
|
||||
customOpacity = Style.opacityNone
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fadeInTimer
|
||||
interval: 100
|
||||
onTriggered: customOpacity = Style.opacityHeavy
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "quickshell-dimmer"
|
||||
|
||||
// mask: Region {}
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
right: true
|
||||
left: true
|
||||
}
|
||||
|
||||
color: Qt.alpha(Color.mShadow, customOpacity)
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ Variants {
|
||||
delegate: Loader {
|
||||
required property ShellScreen modelData
|
||||
|
||||
active: Settings.isLoaded && CompositorService.isNiri && modelData && Settings.data.wallpaper.enabled
|
||||
active: CompositorService.isNiri && CompositorService.niriOverviewActive && modelData && Settings.data.wallpaper.enabled
|
||||
|
||||
property string wallpaper: ""
|
||||
|
||||
@@ -21,6 +21,10 @@ Variants {
|
||||
if (modelData) {
|
||||
Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
|
||||
}
|
||||
updateWallpaper()
|
||||
}
|
||||
|
||||
function updateWallpaper() {
|
||||
wallpaper = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
|
||||
}
|
||||
|
||||
@@ -34,6 +38,15 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
function onIsInitializedChanged() {
|
||||
if (WallpaperService.isInitialized) {
|
||||
updateWallpaper()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: Color.transparent
|
||||
screen: modelData
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
|
||||
@@ -48,10 +48,10 @@ Loader {
|
||||
margins {
|
||||
// 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 && ((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 && ((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 && ((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 && ((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
|
||||
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 {}
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Notification
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
@@ -27,7 +28,7 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
active: BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
screen: modelData || null
|
||||
@@ -46,11 +47,18 @@ Variants {
|
||||
}
|
||||
|
||||
// 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.marginVertical * Style.marginXL * scaling : 0
|
||||
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
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 {
|
||||
@@ -68,6 +76,17 @@ Variants {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent
|
||||
@@ -88,7 +107,7 @@ Variants {
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: NWidgetLoader {
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
@@ -111,7 +130,7 @@ Variants {
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: NWidgetLoader {
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
@@ -135,7 +154,7 @@ Variants {
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: NWidgetLoader {
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
@@ -169,7 +188,7 @@ Variants {
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: NWidgetLoader {
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
@@ -194,7 +213,7 @@ Variants {
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: NWidgetLoader {
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
@@ -220,7 +239,7 @@ Variants {
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: NWidgetLoader {
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
|
||||
@@ -22,7 +22,7 @@ ColumnLayout {
|
||||
|
||||
NText {
|
||||
text: root.label
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mSecondary
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.fillWidth: true
|
||||
@@ -67,7 +67,7 @@ ColumnLayout {
|
||||
// One device BT icon
|
||||
NIcon {
|
||||
icon: BluetoothService.getDeviceIcon(modelData)
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
@@ -79,7 +79,7 @@ ColumnLayout {
|
||||
// Device name
|
||||
NText {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
@@ -90,7 +90,7 @@ ColumnLayout {
|
||||
NText {
|
||||
text: BluetoothService.getStatusString(modelData)
|
||||
visible: text !== ""
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
|
||||
@@ -103,21 +103,21 @@ ColumnLayout {
|
||||
// Device signal strength - "Unknown" when not connected
|
||||
NText {
|
||||
text: BluetoothService.getSignalStrength(modelData)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
text: BluetoothService.getSignalIcon(modelData)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
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 + "%" : ""
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ ColumnLayout {
|
||||
NText {
|
||||
visible: modelData.batteryAvailable
|
||||
text: BluetoothService.getBattery(modelData)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ ColumnLayout {
|
||||
}
|
||||
return "Connect"
|
||||
}
|
||||
icon: (isBusy ? "hourglass-split" : null)
|
||||
icon: (isBusy ? "busy" : null)
|
||||
onClicked: {
|
||||
if (modelData.connected) {
|
||||
BluetoothService.disconnectDevice(modelData)
|
||||
@@ -176,46 +176,6 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea {
|
||||
|
||||
// id: availableDeviceArea
|
||||
// acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
// anchors.fill: parent
|
||||
// hoverEnabled: true
|
||||
// cursorShape: (canConnect || canDisconnect)
|
||||
// && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
||||
// onEntered: {
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.show()
|
||||
// }
|
||||
// }
|
||||
// onExited: {
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.hide()
|
||||
// }
|
||||
// }
|
||||
// onClicked: function (mouse) {
|
||||
|
||||
// if (!modelData || modelData.pairing) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.hide()
|
||||
// }
|
||||
|
||||
// if (mouse.button === Qt.LeftButton) {
|
||||
// if (modelData.connected) {
|
||||
// BluetoothService.disconnectDevice(modelData)
|
||||
// } else {
|
||||
// BluetoothService.connectDeviceWithTrust(modelData)
|
||||
// }
|
||||
// } else if (mouse.button === Qt.RightButton) {
|
||||
// BluetoothService.forgetDevice(modelData)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,13 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "bluetooth"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Bluetooth"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("bluetooth.panel.title")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
@@ -44,15 +44,15 @@ NPanel {
|
||||
|
||||
NToggle {
|
||||
id: bluetoothSwitch
|
||||
checked: Settings.data.network.bluetoothEnabled
|
||||
checked: BluetoothService.enabled
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.65 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
enabled: Settings.data.network.bluetoothEnabled
|
||||
enabled: BluetoothService.enabled
|
||||
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
|
||||
tooltipText: "Refresh Devices"
|
||||
tooltipText: I18n.tr("tooltips.refresh-devices")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
@@ -63,7 +63,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
@@ -88,21 +88,21 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "bluetooth-off"
|
||||
font.pointSize: 64 * scaling
|
||||
pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Bluetooth is disabled"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("bluetooth.panel.disabled")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Enable Bluetooth to see available devices."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
text: I18n.tr("bluetooth.panel.enable-message")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
@@ -124,7 +124,7 @@ NPanel {
|
||||
|
||||
// Connected devices
|
||||
BluetoothDevicesList {
|
||||
label: "Connected devices"
|
||||
label: I18n.tr("bluetooth.panel.connected-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
@@ -138,8 +138,8 @@ NPanel {
|
||||
|
||||
// Known devices
|
||||
BluetoothDevicesList {
|
||||
label: "Known devices"
|
||||
tooltipText: "Left click to connect.\nRight click to forget."
|
||||
label: I18n.tr("bluetooth.panel.known-devices")
|
||||
tooltipText: I18n.tr("tooltips.connect-disconnect-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
@@ -153,7 +153,7 @@ NPanel {
|
||||
|
||||
// Available devices
|
||||
BluetoothDevicesList {
|
||||
label: "Available devices"
|
||||
label: I18n.tr("bluetooth.panel.available-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
@@ -186,7 +186,7 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "refresh"
|
||||
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
|
||||
pointSize: Style.fontSizeXXL * 1.5 * scaling
|
||||
color: Color.mPrimary
|
||||
|
||||
RotationAnimation on rotation {
|
||||
@@ -199,15 +199,15 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Scanning for devices..."
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("bluetooth.panel.scanning")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Make sure your device is in pairing mode."
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
text: I18n.tr("bluetooth.panel.pairing-mode")
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
270
Modules/Bar/Calendar/CalendarPanel.qml
Normal file
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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -7,6 +8,8 @@ import qs.Widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
@@ -43,6 +46,7 @@ Item {
|
||||
Component {
|
||||
id: verticalPillComponent
|
||||
BarPillVertical {
|
||||
screen: root.screen
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
@@ -68,6 +72,7 @@ Item {
|
||||
Component {
|
||||
id: horizontalPillComponent
|
||||
BarPillHorizontal {
|
||||
screen: root.screen
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -7,6 +8,8 @@ import qs.Widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
@@ -20,7 +23,7 @@ Item {
|
||||
property bool compact: false
|
||||
|
||||
// Effective shown state (true if hovered/animated open or forced)
|
||||
readonly property bool revealed: forceOpen || showPill
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
@@ -46,8 +49,18 @@ Item {
|
||||
width: pillHeight + Math.max(0, pill.width - pillOverlap)
|
||||
height: pillHeight
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onTooltipTextChanged() {
|
||||
TooltipService.updateText(root.tooltipText)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
width: revealed ? pillMaxWidth : 1
|
||||
height: pillHeight
|
||||
|
||||
@@ -77,8 +90,8 @@ Item {
|
||||
return centerX + offset
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightBold
|
||||
color: forceOpen ? Color.mOnSurface : Color.mPrimary
|
||||
visible: revealed
|
||||
@@ -119,7 +132,7 @@ Item {
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
font.pointSize: iconSize
|
||||
pointSize: iconSize
|
||||
color: hovered ? Color.mOnTertiary : Color.mOnSurface
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
@@ -195,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
|
||||
@@ -220,8 +225,8 @@ Item {
|
||||
onEntered: {
|
||||
hovered = true
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
if (disableOpen) {
|
||||
TooltipService.show(pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
|
||||
if (disableOpen || forceClose) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
@@ -231,10 +236,10 @@ Item {
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen) {
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
TooltipService.hide()
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -7,6 +8,7 @@ import qs.Widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
@@ -58,8 +60,18 @@ Item {
|
||||
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
|
||||
|
||||
@@ -91,8 +103,8 @@ Item {
|
||||
return offset
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -144,7 +156,7 @@ Item {
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
font.pointSize: iconSize
|
||||
pointSize: iconSize
|
||||
color: hovered ? Color.mOnTertiary : Color.mOnSurface
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
@@ -236,16 +248,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
target: pill
|
||||
text: root.tooltipText
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
delay: Style.tooltipDelayLong
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
@@ -263,7 +265,7 @@ Item {
|
||||
onEntered: {
|
||||
hovered = true
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
TooltipService.show(pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
|
||||
if (disableOpen || forceClose) {
|
||||
return
|
||||
}
|
||||
@@ -277,7 +279,7 @@ Item {
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
TooltipService.hide()
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
|
||||
@@ -8,12 +8,14 @@ Item {
|
||||
|
||||
property string widgetId: ""
|
||||
property var widgetProps: ({})
|
||||
property bool enabled: true
|
||||
property string screenName: widgetProps.screen ? widgetProps.screen.name : ""
|
||||
property string section: widgetProps.section || ""
|
||||
property int sectionIndex: widgetProps.sectionWidgetIndex || 0
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if (loader.item && loader.item.screen && screenName === loader.item.screen.name) {
|
||||
function onScaleChanged(aScreenName, scale) {
|
||||
if (loader.item && loader.item.screen && aScreenName === screenName) {
|
||||
loader.item['scaling'] = scale
|
||||
}
|
||||
}
|
||||
@@ -27,7 +29,7 @@ Item {
|
||||
id: loader
|
||||
|
||||
anchors.fill: parent
|
||||
active: Settings.isLoaded && enabled && widgetId !== ""
|
||||
active: widgetId !== ""
|
||||
sourceComponent: {
|
||||
if (!active) {
|
||||
return null
|
||||
@@ -45,18 +47,30 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Register this widget instance with BarService
|
||||
if (screenName && section) {
|
||||
BarService.registerWidget(screenName, section, widgetId, sectionIndex, item)
|
||||
}
|
||||
|
||||
if (item.hasOwnProperty("onLoaded")) {
|
||||
item.onLoaded()
|
||||
}
|
||||
|
||||
Logger.log("NWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
|
||||
//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("WidgetLoader", "Widget not found in registry:", widgetId)
|
||||
Logger.warn("BarWidgetLoader", "Widget not found in bar registry:", widgetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ PopupWindow {
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
}
|
||||
@@ -175,7 +175,7 @@ PopupWindow {
|
||||
|
||||
NIcon {
|
||||
icon: modelData?.hasChildren ? "menu" : ""
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
|
||||
@@ -235,13 +235,13 @@ PopupWindow {
|
||||
openLeft = false
|
||||
} else {
|
||||
// Bar is horizontal (top/bottom) or undefined, use space-based logic
|
||||
openLeft = (globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width))
|
||||
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 ? screen.width : Screen.width)) {
|
||||
} else if (!openLeft && globalPos.x + entry.width + submenuWidth > screen.width) {
|
||||
// Would open off the right edge, force left opening
|
||||
openLeft = true
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: Settings.data.network.wifiEnabled ? "wifi" : "wifi-off"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Wi-Fi"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("wifi.panel.title")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
@@ -56,7 +56,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: "Refresh"
|
||||
tooltipText: I18n.tr("tooltips.refresh")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
|
||||
onClicked: NetworkService.scan()
|
||||
@@ -64,7 +64,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
@@ -92,14 +92,14 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "warning"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mError
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NetworkService.lastError
|
||||
color: Color.mError
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -130,21 +130,21 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "wifi-off"
|
||||
font.pointSize: 64 * scaling
|
||||
pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Wi-Fi is disabled"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("wifi.panel.disabled")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Enable Wi-Fi to see available networks."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
text: I18n.tr("wifi.panel.enable-message")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
@@ -172,8 +172,8 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Searching for nearby networks..."
|
||||
font.pointSize: Style.fontSizeNormal * scaling
|
||||
text: I18n.tr("wifi.panel.searching")
|
||||
pointSize: Style.fontSizeNormal * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
@@ -242,7 +242,7 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: NetworkService.signalIcon(modelData.signal)
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ NPanel {
|
||||
|
||||
NText {
|
||||
text: modelData.ssid
|
||||
font.pointSize: Style.fontSizeNormal * scaling
|
||||
pointSize: Style.fontSizeNormal * scaling
|
||||
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
@@ -263,20 +263,22 @@ NPanel {
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NText {
|
||||
text: `${modelData.signal}%`
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
text: I18n.tr("system.signal-strength", {
|
||||
"signal": modelData.signal
|
||||
})
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
@@ -295,8 +297,8 @@ NPanel {
|
||||
NText {
|
||||
id: connectedText
|
||||
anchors.centerIn: parent
|
||||
text: "Connected"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
text: I18n.tr("wifi.panel.connected")
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
@@ -311,8 +313,8 @@ NPanel {
|
||||
NText {
|
||||
id: disconnectingText
|
||||
anchors.centerIn: parent
|
||||
text: "Disconnecting..."
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
text: I18n.tr("wifi.panel.disconnecting")
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
@@ -327,8 +329,8 @@ NPanel {
|
||||
NText {
|
||||
id: forgettingText
|
||||
anchors.centerIn: parent
|
||||
text: "Forgetting..."
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
text: I18n.tr("wifi.panel.forgetting")
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
@@ -345,8 +347,8 @@ NPanel {
|
||||
NText {
|
||||
id: savedText
|
||||
anchors.centerIn: parent
|
||||
text: "Saved"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
text: I18n.tr("wifi.panel.saved")
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
@@ -367,7 +369,7 @@ NPanel {
|
||||
NIconButton {
|
||||
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
icon: "trash"
|
||||
tooltipText: "Forget network"
|
||||
tooltipText: I18n.tr("tooltips.forget-network")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
|
||||
}
|
||||
@@ -376,10 +378,10 @@ NPanel {
|
||||
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 "Connect"
|
||||
return I18n.tr("wifi.panel.connect")
|
||||
if (!NetworkService.isSecured(modelData.security))
|
||||
return "Connect"
|
||||
return "Password"
|
||||
return I18n.tr("wifi.panel.connect")
|
||||
return I18n.tr("wifi.panel.password")
|
||||
}
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
@@ -397,7 +399,7 @@ NPanel {
|
||||
|
||||
NButton {
|
||||
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
text: "Disconnect"
|
||||
text: I18n.tr("wifi.panel.disconnect")
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
@@ -437,6 +439,7 @@ NPanel {
|
||||
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
|
||||
@@ -454,18 +457,18 @@ NPanel {
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
NText {
|
||||
visible: parent.text.length === 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Enter password..."
|
||||
text: I18n.tr("wifi.panel.enter-password")
|
||||
color: Color.mOnSurfaceVariant
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Connect"
|
||||
text: I18n.tr("wifi.panel.connect")
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
enabled: passwordInput.length > 0 && !NetworkService.connecting
|
||||
outlined: true
|
||||
@@ -506,13 +509,13 @@ NPanel {
|
||||
RowLayout {
|
||||
NIcon {
|
||||
icon: "trash"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mError
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Forget this network?"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
text: I18n.tr("wifi.panel.forget-network")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mError
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -520,7 +523,7 @@ NPanel {
|
||||
|
||||
NButton {
|
||||
id: forgetButton
|
||||
text: "Forget"
|
||||
text: I18n.tr("wifi.panel.forget")
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
outlined: forgetButton.hovered ? false : true
|
||||
@@ -555,20 +558,20 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "search"
|
||||
font.pointSize: 64 * scaling
|
||||
pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "No networks found"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("wifi.panel.no-networks")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Scan again"
|
||||
text: I18n.tr("wifi.panel.scan-again")
|
||||
icon: "refresh"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: NetworkService.scan()
|
||||
@@ -30,60 +30,34 @@ Item {
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
|
||||
|
||||
// 6% of total width
|
||||
readonly property real minWidth: Math.max(1, screen.width * 0.06)
|
||||
readonly property real maxWidth: minWidth * 2
|
||||
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 isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
|
||||
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
// 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")
|
||||
|
||||
readonly property real textSize: {
|
||||
var base = isVertical ? width : height
|
||||
return Math.max(1, compact ? base * 0.43 : base * 0.33)
|
||||
}
|
||||
// Fixed width
|
||||
readonly property real widgetWidth: Math.max(145, screen.width * 0.06)
|
||||
|
||||
readonly property real iconSize: textSize * 1.25
|
||||
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
|
||||
|
||||
function getTitle() {
|
||||
try {
|
||||
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error getting title:", e)
|
||||
return ""
|
||||
opacity: !autoHide || hasActiveWindow ? 1.0 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
visible: getTitle() !== ""
|
||||
|
||||
function calculatedVerticalHeight() {
|
||||
// Use standard widget height like other widgets
|
||||
return Math.round(Style.capsuleHeight * scaling)
|
||||
}
|
||||
|
||||
function calculatedHorizontalWidth() {
|
||||
let total = Style.marginM * 2 * scaling // internal padding
|
||||
|
||||
if (showIcon) {
|
||||
total += Style.capsuleHeight * 0.5 * scaling + 2 * scaling // icon + spacing
|
||||
}
|
||||
|
||||
// Calculate actual text width more accurately
|
||||
const title = getTitle()
|
||||
if (title !== "") {
|
||||
// Estimate text width: average character width * number of characters
|
||||
const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate
|
||||
const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling)
|
||||
total += titleWidth
|
||||
}
|
||||
|
||||
// Row layout handles spacing between widgets
|
||||
return Math.max(total, Style.capsuleHeight * scaling) // Minimum width
|
||||
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
|
||||
}
|
||||
|
||||
function getAppIcon() {
|
||||
@@ -94,7 +68,7 @@ Item {
|
||||
try {
|
||||
const idValue = focusedWindow.appId
|
||||
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
|
||||
const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase())
|
||||
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase())
|
||||
if (iconResult && iconResult !== "") {
|
||||
return iconResult
|
||||
}
|
||||
@@ -103,49 +77,49 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = AppIcons.iconForAppId(normalizedId2.toLowerCase())
|
||||
if (iconResult2 && iconResult2 !== "") {
|
||||
return iconResult2
|
||||
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)
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return ThemeIcons.iconFromName(fallbackIcon)
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
|
||||
return ""
|
||||
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: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
text: windowTitle
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: windowTitleRect
|
||||
id: windowActiveRect
|
||||
visible: root.visible
|
||||
anchors.left: (barPosition === "top" || barPosition === "bottom") ? parent.left : undefined
|
||||
anchors.top: (barPosition === "left" || barPosition === "right") ? parent.top : undefined
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: width / 2
|
||||
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 {
|
||||
@@ -153,21 +127,21 @@ Item {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
clip: true
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: horizontalLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 2 * scaling
|
||||
id: rowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
z: 1
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
Layout.preferredWidth: Style.capsuleHeight * 0.75 * scaling
|
||||
Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling
|
||||
Layout.preferredWidth: Math.round(18 * scaling)
|
||||
Layout.preferredHeight: Math.round(18 * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: getTitle() !== "" && showIcon
|
||||
visible: showIcon
|
||||
|
||||
IconImage {
|
||||
id: windowIcon
|
||||
@@ -176,39 +150,139 @@ Item {
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
|
||||
// Handle loading errors gracefully
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
Logger.warn("ActiveWindow", "Failed to load icon:", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
// Title container with scrolling
|
||||
Item {
|
||||
id: titleContainer
|
||||
Layout.preferredWidth: {
|
||||
try {
|
||||
if (mouseArea.containsMouse) {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||
} else {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars
|
||||
// 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
|
||||
|
||||
clip: true
|
||||
|
||||
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
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error calculating width:", e)
|
||||
return 80 * scaling
|
||||
}
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
clip: true
|
||||
|
||||
// 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 {
|
||||
@@ -223,15 +297,17 @@ Item {
|
||||
Item {
|
||||
id: verticalLayout
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.marginXS * scaling * 2
|
||||
height: parent.height - Style.marginXS * scaling * 2
|
||||
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.capsuleHeight * 0.75 * scaling
|
||||
height: Style.capsuleHeight * 0.75 * scaling
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: Style.baseWidgetSize * 0.5 * scaling
|
||||
anchors.centerIn: parent
|
||||
visible: windowTitle !== ""
|
||||
|
||||
IconImage {
|
||||
id: windowIconVertical
|
||||
@@ -240,13 +316,6 @@ Item {
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
|
||||
// Handle loading errors gracefully
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
Logger.warn("ActiveWindow", "Failed to load icon:", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,27 +326,16 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onEntered: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.show()
|
||||
if ((windowTitle !== "") && (barPosition === "left" || barPosition === "right") || (scrollingMode === "never")) {
|
||||
TooltipService.show(root, windowTitle, BarService.getTooltipDirection())
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.hide()
|
||||
}
|
||||
TooltipService.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Hover tooltip with full title (only for vertical bars)
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
target: verticalLayout
|
||||
text: getTitle()
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,9 @@ Item {
|
||||
// Only notify once we are a below threshold
|
||||
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
|
||||
root.hasNotifiedLowBattery = true
|
||||
ToastService.showWarning("Low Battery", `Battery is at ${Math.round(percent)}%. Please connect the charger.`)
|
||||
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
|
||||
@@ -85,8 +87,9 @@ Item {
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarWidgetRegistry.getPillDirection(root)
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
|
||||
text: (isReady || testMode) ? Math.round(percent) : "-"
|
||||
suffix: "%"
|
||||
|
||||
@@ -19,9 +19,9 @@ NIconButton {
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off"
|
||||
tooltipText: "Bluetooth devices."
|
||||
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,10 +1,10 @@
|
||||
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
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -77,8 +77,9 @@ Item {
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarWidgetRegistry.getPillDirection(root)
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: getIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: {
|
||||
|
||||
@@ -29,179 +29,83 @@ Rectangle {
|
||||
}
|
||||
|
||||
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 use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
|
||||
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
|
||||
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
|
||||
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
|
||||
|
||||
// Use compact mode for vertical bars
|
||||
readonly property bool verticalMode: barPosition === "left" || barPosition === "right"
|
||||
implicitWidth: isBarVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.marginM * 2 * scaling)
|
||||
|
||||
implicitWidth: verticalMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match BarPill
|
||||
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.fill: parent
|
||||
anchors.margins: compact ? 0 : Style.marginXS * scaling
|
||||
anchors.centerIn: parent
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
// Horizontal
|
||||
Loader {
|
||||
id: horizontalLoader
|
||||
active: !isBarVertical
|
||||
anchors.centerIn: parent
|
||||
spacing: verticalMode ? -2 * scaling : -3 * scaling
|
||||
|
||||
// Compact mode for vertical bars - Time section (HH, MM)
|
||||
Repeater {
|
||||
model: verticalMode ? 2 : 1
|
||||
NText {
|
||||
readonly property bool showSeconds: (displayFormat === "time-seconds")
|
||||
readonly property bool inlineDate: (displayFormat === "time-date")
|
||||
readonly property var now: Time.date
|
||||
|
||||
text: {
|
||||
if (verticalMode) {
|
||||
// Compact mode: time section (first 2 lines)
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Hours
|
||||
if (use12h) {
|
||||
const hours = now.getHours()
|
||||
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
||||
return displayHours.toString().padStart(2, '0')
|
||||
} else {
|
||||
return now.getHours().toString().padStart(2, '0')
|
||||
}
|
||||
case 1:
|
||||
// Minutes
|
||||
return now.getMinutes().toString().padStart(2, '0')
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
// Normal mode: single line with time
|
||||
let timeStr = ""
|
||||
|
||||
if (use12h) {
|
||||
// 12-hour format with proper padding and consistent spacing
|
||||
const hours = now.getHours()
|
||||
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
||||
const paddedHours = displayHours.toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
const ampm = hours < 12 ? 'AM' : 'PM'
|
||||
|
||||
if (showSeconds) {
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
|
||||
} else {
|
||||
timeStr = `${paddedHours}:${minutes} ${ampm}`
|
||||
}
|
||||
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 {
|
||||
// 24-hour format with padding
|
||||
const hours = now.getHours().toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
|
||||
if (showSeconds) {
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||
timeStr = `${hours}:${minutes}:${seconds}`
|
||||
} else {
|
||||
timeStr = `${hours}:${minutes}`
|
||||
}
|
||||
}
|
||||
|
||||
// Add inline date if needed
|
||||
if (inlineDate) {
|
||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||
const day = now.getDate().toString().padStart(2, '0')
|
||||
let month = now.toLocaleDateString(Qt.locale(), "MMM")
|
||||
timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
|
||||
}
|
||||
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: verticalMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Separator line for compact mode (between time and date)
|
||||
Rectangle {
|
||||
visible: verticalMode
|
||||
Layout.preferredWidth: 20 * scaling
|
||||
Layout.preferredHeight: 2 * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 3 * scaling
|
||||
Layout.bottomMargin: 3 * scaling
|
||||
color: Color.mPrimary
|
||||
opacity: 0.3
|
||||
radius: 1 * scaling
|
||||
}
|
||||
|
||||
// Compact mode for vertical bars - Date section (DD, MM)
|
||||
Repeater {
|
||||
model: verticalMode ? 2 : 0
|
||||
NText {
|
||||
readonly property var now: Time.date
|
||||
|
||||
text: {
|
||||
if (verticalMode) {
|
||||
// Compact mode: date section (last 2 lines)
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Day
|
||||
return now.getDate().toString().padStart(2, '0')
|
||||
case 1:
|
||||
// Month
|
||||
return (now.getMonth() + 1).toString().padStart(2, '0')
|
||||
default:
|
||||
return ""
|
||||
return (index == 0) ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
|
||||
}
|
||||
}
|
||||
return ""
|
||||
font.weight: Style.fontWeightBold
|
||||
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Second line for normal mode (date)
|
||||
NText {
|
||||
visible: !verticalMode && (displayFormat === "time-date-short")
|
||||
text: {
|
||||
const now = Time.date
|
||||
const day = now.getDate().toString().padStart(2, '0')
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0')
|
||||
return reverseDayMonth ? `${month}/${day}` : `${day}/${month}`
|
||||
}
|
||||
|
||||
// Enable fixed-width font for consistent spacing
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightRegular
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: `${Time.formatDate(reverseDayMonth)}.`
|
||||
target: clockContainer
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
// 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 {
|
||||
@@ -211,14 +115,14 @@ Rectangle {
|
||||
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()
|
||||
TooltipService.hide()
|
||||
PanelService.getPanel("calendarPanel")?.toggle(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,14 @@ NIconButton {
|
||||
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 || ""
|
||||
|
||||
icon: useDistroLogo ? "" : "noctalia"
|
||||
tooltipText: "Open side panel."
|
||||
// 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)
|
||||
@@ -40,16 +44,22 @@ NIconButton {
|
||||
colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent
|
||||
onClicked: PanelService.getPanel("sidePanel")?.toggle(this)
|
||||
onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
|
||||
|
||||
IconImage {
|
||||
id: logo
|
||||
id: customOrDistroLogo
|
||||
anchors.centerIn: parent
|
||||
width: root.width * 0.8
|
||||
height: width
|
||||
source: useDistroLogo ? DistroLogoService.osLogo : ""
|
||||
visible: useDistroLogo && source !== ""
|
||||
source: {
|
||||
if (customIconPath !== "")
|
||||
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath
|
||||
if (useDistroLogo)
|
||||
return DistroLogoService.osLogo
|
||||
return ""
|
||||
}
|
||||
visible: source !== ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Item {
|
||||
@@ -47,7 +47,8 @@ Item {
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
rightOpen: BarWidgetRegistry.getPillDirection(root)
|
||||
screen: root.screen
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: customIcon
|
||||
text: _dynamicText
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
@@ -57,7 +58,7 @@ Item {
|
||||
disableOpen: true
|
||||
tooltipText: {
|
||||
if (!hasExec) {
|
||||
return "Custom Button - Configure in settings"
|
||||
return "Custom button, configure in settings."
|
||||
} else {
|
||||
var lines = []
|
||||
if (leftClickExec !== "") {
|
||||
|
||||
@@ -10,7 +10,8 @@ NIconButton {
|
||||
property real scaling: 1.0
|
||||
|
||||
icon: "dark-mode"
|
||||
tooltipText: "Toggle light/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
|
||||
@@ -14,9 +14,11 @@ NIconButton {
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -42,19 +42,21 @@ Item {
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarWidgetRegistry.getPillDirection(root)
|
||||
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: "Keyboard layout: " + 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,13 +33,43 @@ Item {
|
||||
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
|
||||
|
||||
// 6% of total width
|
||||
readonly property real minWidth: Math.max(1, screen.width * 0.06)
|
||||
readonly property real maxWidth: minWidth * 2
|
||||
// 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}` : "")
|
||||
@@ -49,23 +79,6 @@ Item {
|
||||
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
|
||||
}
|
||||
|
||||
function calculatedHorizontalWidth() {
|
||||
let total = Style.marginM * 2 * scaling // internal padding
|
||||
if (showAlbumArt) {
|
||||
total += 18 * scaling + 2 * scaling // album art + spacing
|
||||
} else {
|
||||
total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing
|
||||
}
|
||||
total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text
|
||||
// Row layout handles spacing between widgets
|
||||
return total
|
||||
}
|
||||
|
||||
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) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0
|
||||
|
||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||
|
||||
// A hidden text element to safely measure the full title width
|
||||
NText {
|
||||
id: fullTitleMetrics
|
||||
@@ -79,18 +92,11 @@ Item {
|
||||
visible: root.visible
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
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
|
||||
|
||||
// Used to anchor the tooltip, so the tooltip does not move when the content expands
|
||||
Item {
|
||||
id: anchor
|
||||
height: parent.height
|
||||
width: 200 * scaling
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
@@ -100,14 +106,14 @@ Item {
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && 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
|
||||
}
|
||||
}
|
||||
@@ -115,14 +121,14 @@ Item {
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
|
||||
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.mOnSurfaceVariant
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
@@ -130,14 +136,14 @@ Item {
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
|
||||
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.mOnSurfaceVariant
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
@@ -145,23 +151,25 @@ Item {
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
visible: (barPosition === "top" || barPosition === "bottom")
|
||||
z: 1 // Above the visualizer
|
||||
|
||||
NIcon {
|
||||
id: windowIcon
|
||||
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
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
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible
|
||||
visible: !hasActivePlayer || (!showAlbumArt && !trackArt.visible)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showAlbumArt
|
||||
visible: showAlbumArt && hasActivePlayer
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
@@ -180,24 +188,143 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
|
||||
Item {
|
||||
id: titleContainer
|
||||
Layout.preferredWidth: {
|
||||
if (mouseArea.containsMouse) {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||
} else {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
||||
// 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
|
||||
|
||||
clip: true
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mSecondary
|
||||
// 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 {
|
||||
@@ -222,13 +349,13 @@ Item {
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: Style.baseWidgetSize * 0.5 * scaling
|
||||
anchors.centerIn: parent
|
||||
visible: getTitle() !== ""
|
||||
|
||||
NIcon {
|
||||
id: mediaIconVertical
|
||||
anchors.fill: parent
|
||||
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
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
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
@@ -240,9 +367,13 @@ Item {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
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) {
|
||||
@@ -257,43 +388,15 @@ Item {
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.show()
|
||||
} else if (tooltip.text !== "") {
|
||||
tooltip.show()
|
||||
var textToShow = hasActivePlayer ? tooltipText : placeholderText
|
||||
if ((textToShow !== "") && (barPosition === "left" || barPosition === "right") || (scrollingMode === "never")) {
|
||||
TooltipService.show(root, textToShow, BarService.getTooltipDirection())
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.hide()
|
||||
} else {
|
||||
tooltip.hide()
|
||||
}
|
||||
TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
return getTitle()
|
||||
} else {
|
||||
var str = ""
|
||||
if (MediaService.canGoNext) {
|
||||
str += "Right click for next.\n"
|
||||
}
|
||||
if (MediaService.canGoPrevious) {
|
||||
str += "Middle click for previous."
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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
|
||||
@@ -89,15 +89,19 @@ Item {
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
rightOpen: BarWidgetRegistry.getPillDirection(root)
|
||||
|
||||
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.floor(AudioService.inputVolume * 100)
|
||||
text: Math.round(AudioService.inputVolume * 100)
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume."
|
||||
tooltipText: I18n.tr("tooltips.microphone-volume-at", {
|
||||
"volume": Math.round(AudioService.inputVolume * 100)
|
||||
})
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
|
||||
@@ -4,7 +4,7 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -22,8 +22,15 @@ NIconButton {
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
|
||||
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "forced." : "enabled.") : "disabled."}\nLeft click to cycle (disabled → normal → forced).\nRight click to access settings.`
|
||||
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
|
||||
|
||||
@@ -39,7 +39,7 @@ NIconButton {
|
||||
function computeUnreadCount() {
|
||||
var since = lastSeenTs()
|
||||
var count = 0
|
||||
var model = NotificationService.historyModel
|
||||
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
|
||||
@@ -52,7 +52,8 @@ NIconButton {
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
|
||||
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
|
||||
|
||||
@@ -11,45 +11,19 @@ NIconButton {
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
readonly property bool hasPP: PowerProfileService.available
|
||||
|
||||
baseSize: Style.capsuleHeight
|
||||
visible: hasPP
|
||||
visible: PowerProfileService.available
|
||||
|
||||
function profileIcon() {
|
||||
if (!hasPP)
|
||||
return "balanced"
|
||||
if (PowerProfileService.profile === PowerProfile.Performance)
|
||||
return "performance"
|
||||
if (PowerProfileService.profile === PowerProfile.Balanced)
|
||||
return "balanced"
|
||||
if (PowerProfileService.profile === PowerProfile.PowerSaver)
|
||||
return "powersaver"
|
||||
}
|
||||
|
||||
function profileName() {
|
||||
if (!hasPP)
|
||||
return "Unknown"
|
||||
if (PowerProfileService.profile === PowerProfile.Performance)
|
||||
return "Performance"
|
||||
if (PowerProfileService.profile === PowerProfile.Balanced)
|
||||
return "Balanced"
|
||||
if (PowerProfileService.profile === PowerProfile.PowerSaver)
|
||||
return "Power Saver"
|
||||
}
|
||||
|
||||
function changeProfile() {
|
||||
if (!hasPP)
|
||||
return
|
||||
PowerProfileService.cycleProfile()
|
||||
}
|
||||
|
||||
icon: root.profileIcon()
|
||||
tooltipText: root.profileName()
|
||||
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
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: 1.0
|
||||
|
||||
visible: ScreenRecorderService.isRecording
|
||||
icon: "camera-video"
|
||||
tooltipText: "Screen recording is active\nClick to stop recording"
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
onClicked: ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
@@ -14,10 +14,11 @@ NIconButton {
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
icon: "power"
|
||||
tooltipText: "Power Settings"
|
||||
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("powerPanel")?.toggle()
|
||||
onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle()
|
||||
}
|
||||
@@ -39,12 +39,39 @@ Rectangle {
|
||||
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 real iconSize: textSize * 1.25
|
||||
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)
|
||||
@@ -55,18 +82,15 @@ Rectangle {
|
||||
GridLayout {
|
||||
id: mainGrid
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Dynamic layout based on bar orientation
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? -1 : 1
|
||||
columns: isVertical ? 1 : -1
|
||||
|
||||
rowSpacing: isVertical ? (Style.marginS * scaling) : (Style.marginXS * scaling)
|
||||
columnSpacing: isVertical ? (Style.marginXS * scaling) : (Style.marginXS * scaling)
|
||||
rowSpacing: isVertical ? (Style.marginM * scaling) : 0
|
||||
columnSpacing: isVertical ? 0 : (Style.marginM * scaling)
|
||||
|
||||
// CPU Usage Component
|
||||
Item {
|
||||
Layout.preferredWidth: cpuUsageContent.implicitWidth
|
||||
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
|
||||
@@ -80,32 +104,34 @@ Rectangle {
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-usage"
|
||||
pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: isVertical ? `${Math.round(SystemStatService.cpuUsage)}%` : `${SystemStatService.cpuUsage}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
text: `${Math.round(SystemStatService.cpuUsage)}%`
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-usage"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPU Temperature Component
|
||||
Item {
|
||||
Layout.preferredWidth: cpuTempContent.implicitWidth
|
||||
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
|
||||
@@ -119,32 +145,34 @@ Rectangle {
|
||||
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
|
||||
}
|
||||
|
||||
NText {
|
||||
text: isVertical ? `${SystemStatService.cpuTemp}°` : `${SystemStatService.cpuTemp}°C`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
text: `${Math.round(SystemStatService.cpuTemp)}°`
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-temperature"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Memory Usage Component
|
||||
Item {
|
||||
Layout.preferredWidth: memoryContent.implicitWidth
|
||||
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
|
||||
@@ -158,38 +186,34 @@ Rectangle {
|
||||
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: {
|
||||
if (showMemoryAsPercent) {
|
||||
return `${SystemStatService.memPercent}%`
|
||||
} else {
|
||||
return isVertical ? `${Math.round(SystemStatService.memGb)}G` : `${SystemStatService.memGb}G`
|
||||
}
|
||||
}
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
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
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "memory"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Download Speed Component
|
||||
Item {
|
||||
Layout.preferredWidth: downloadContent.implicitWidth
|
||||
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
|
||||
@@ -201,34 +225,36 @@ Rectangle {
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * 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)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "download-speed"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Upload Speed Component
|
||||
Item {
|
||||
Layout.preferredWidth: uploadContent.implicitWidth
|
||||
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
|
||||
@@ -240,34 +266,36 @@ Rectangle {
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * 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)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "upload-speed"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk Usage Component (primary drive)
|
||||
Item {
|
||||
Layout.preferredWidth: diskContent.implicitWidth
|
||||
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
|
||||
@@ -279,27 +307,29 @@ Rectangle {
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * 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}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "storage"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,27 @@ Rectangle {
|
||||
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)
|
||||
@@ -41,36 +58,37 @@ Rectangle {
|
||||
columnSpacing: isVerticalBar ? 0 : Style.marginXXS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: ToplevelManager && ToplevelManager.toplevels ? ToplevelManager.toplevels : []
|
||||
model: CompositorService.windows
|
||||
delegate: Item {
|
||||
id: taskbarItem
|
||||
required property Toplevel modelData
|
||||
property Toplevel toplevel: modelData
|
||||
property bool isActive: ToplevelManager.activeToplevel === modelData
|
||||
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
|
||||
|
||||
Rectangle {
|
||||
id: iconBackground
|
||||
anchors.centerIn: parent
|
||||
IconImage {
|
||||
|
||||
id: appIcon
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: taskbarItem.isActive ? Color.mPrimary : root.color
|
||||
border.width: 0
|
||||
radius: Math.round(Style.radiusXS * root.scaling)
|
||||
border.color: "transparent"
|
||||
z: -1
|
||||
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
opacity: modelData.isFocused ? Style.opacityFull : 0.6
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,27 +104,20 @@ Rectangle {
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
try {
|
||||
taskbarItem.modelData.activate()
|
||||
CompositorService.focusWindow(taskbarItem.modelData.id)
|
||||
} catch (error) {
|
||||
Logger.error("Taskbar", "Failed to activate toplevel: " + error)
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
try {
|
||||
taskbarItem.modelData.close()
|
||||
CompositorService.closeWindow(taskbarItem.modelData.id)
|
||||
} catch (error) {
|
||||
Logger.error("Taskbar", "Failed to close toplevel: " + error)
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: taskbarTooltip.show()
|
||||
onExited: taskbarTooltip.hide()
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: taskbarTooltip
|
||||
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
|
||||
target: taskbarItem
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
onEntered: TooltipService.show(taskbarItem, taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown app.", BarService.getTooltipDirection())
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ Rectangle {
|
||||
|
||||
IconImage {
|
||||
id: trayIcon
|
||||
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: Style.marginL * scaling
|
||||
height: Style.marginL * scaling
|
||||
@@ -102,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) {
|
||||
@@ -135,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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
|
||||
@@ -75,15 +75,18 @@ Item {
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarWidgetRegistry.getPillDirection(root)
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: getIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.floor(AudioService.volume * 100)
|
||||
text: Math.round(AudioService.volume * 100)
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume."
|
||||
tooltipText: I18n.tr("tooltips.volume-at", {
|
||||
"volume": Math.round(AudioService.volume * 100)
|
||||
})
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
|
||||
24
Modules/Bar/Widgets/WallpaperSelector.qml
Normal file
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)
|
||||
}
|
||||
@@ -19,7 +19,8 @@ NIconButton {
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
tooltipText: I18n.tr("tooltips.manage-wifi")
|
||||
tooltipDirection: BarService.getTooltipDirection()
|
||||
icon: {
|
||||
try {
|
||||
if (NetworkService.ethernetConnected) {
|
||||
@@ -40,7 +41,6 @@ NIconButton {
|
||||
return "signal_wifi_bad"
|
||||
}
|
||||
}
|
||||
tooltipText: "Manage Wi-Fi."
|
||||
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -56,6 +57,10 @@ Item {
|
||||
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)
|
||||
|
||||
implicitWidth: isVertical ? Math.round(Style.barHeight * scaling) : computeWidth()
|
||||
@@ -63,18 +68,14 @@ Item {
|
||||
|
||||
function getWorkspaceWidth(ws) {
|
||||
const d = Style.capsuleHeight * root.baseDimensionRatio
|
||||
if (ws.isFocused)
|
||||
return d * 2.5
|
||||
else
|
||||
return d
|
||||
const factor = ws.isFocused ? 2.2 : 1
|
||||
return d * factor * scaling
|
||||
}
|
||||
|
||||
function getWorkspaceHeight(ws) {
|
||||
const d = Style.capsuleHeight * root.baseDimensionRatio
|
||||
if (ws.isFocused)
|
||||
return d * 3
|
||||
else
|
||||
return d
|
||||
const factor = ws.isFocused ? 2.2 : 1
|
||||
return d * factor * scaling
|
||||
}
|
||||
|
||||
function computeWidth() {
|
||||
@@ -99,6 +100,28 @@ Item {
|
||||
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: {
|
||||
refreshWorkspaces()
|
||||
}
|
||||
@@ -111,7 +134,7 @@ Item {
|
||||
onHideUnoccupiedChanged: refreshWorkspaces()
|
||||
|
||||
Connections {
|
||||
target: WorkspaceService
|
||||
target: CompositorService
|
||||
function onWorkspacesChanged() {
|
||||
refreshWorkspaces()
|
||||
}
|
||||
@@ -120,8 +143,8 @@ 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
|
||||
@@ -189,6 +212,46 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -203,7 +266,7 @@ Item {
|
||||
Item {
|
||||
id: workspacePillContainer
|
||||
width: root.getWorkspaceWidth(model)
|
||||
height: Style.capsuleHeight * root.baseDimensionRatio
|
||||
height: Style.capsuleHeight * root.baseDimensionRatio * scaling
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
@@ -212,7 +275,7 @@ Item {
|
||||
Loader {
|
||||
active: (labelMode !== "none")
|
||||
sourceComponent: Component {
|
||||
Text {
|
||||
NText {
|
||||
x: (pill.width - width) / 2
|
||||
y: (pill.height - height) / 2 + (height - contentHeight) / 2
|
||||
text: {
|
||||
@@ -222,9 +285,9 @@ Item {
|
||||
return model.idx.toString()
|
||||
}
|
||||
}
|
||||
font.pointSize: model.isFocused ? workspacePillContainer.height * 0.45 : workspacePillContainer.height * 0.42
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: model.isFocused ? workspacePillContainer.height * 0.45 : workspacePillContainer.height * 0.42
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: Style.fontWeightBold
|
||||
wrapMode: Text.Wrap
|
||||
color: {
|
||||
@@ -235,7 +298,7 @@ Item {
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mOnSecondary
|
||||
|
||||
return Color.mOnSurface
|
||||
return Color.mOnSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,7 +313,7 @@ Item {
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mSecondary
|
||||
|
||||
return Color.mOutline
|
||||
return Qt.alpha(Color.mSecondary, 0.3)
|
||||
}
|
||||
scale: model.isFocused ? 1.0 : 0.9
|
||||
z: 0
|
||||
@@ -260,7 +323,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WorkspaceService.switchToWorkspace(model.idx)
|
||||
CompositorService.switchToWorkspace(model.idx)
|
||||
}
|
||||
hoverEnabled: true
|
||||
}
|
||||
@@ -346,7 +409,7 @@ Item {
|
||||
model: localWorkspaces
|
||||
Item {
|
||||
id: workspacePillContainerVertical
|
||||
width: Style.capsuleHeight * root.baseDimensionRatio
|
||||
width: Style.capsuleHeight * root.baseDimensionRatio * scaling
|
||||
height: root.getWorkspaceHeight(model)
|
||||
|
||||
Rectangle {
|
||||
@@ -356,7 +419,7 @@ Item {
|
||||
Loader {
|
||||
active: (labelMode !== "none")
|
||||
sourceComponent: Component {
|
||||
Text {
|
||||
NText {
|
||||
x: (pillVertical.width - width) / 2
|
||||
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
|
||||
text: {
|
||||
@@ -366,9 +429,9 @@ Item {
|
||||
return model.idx.toString()
|
||||
}
|
||||
}
|
||||
font.pointSize: model.isFocused ? workspacePillContainerVertical.width * 0.45 : workspacePillContainerVertical.width * 0.42
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: model.isFocused ? workspacePillContainerVertical.width * 0.45 : workspacePillContainerVertical.width * 0.42
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: Style.fontWeightBold
|
||||
wrapMode: Text.Wrap
|
||||
color: {
|
||||
@@ -404,7 +467,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WorkspaceService.switchToWorkspace(model.idx)
|
||||
CompositorService.switchToWorkspace(model.idx)
|
||||
}
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
@@ -1,129 +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
|
||||
|
||||
preferredWidth: 340
|
||||
preferredHeight: 320
|
||||
|
||||
// Main Column
|
||||
panelContent: ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
// 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: "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
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron-right"
|
||||
tooltipText: "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.marginM * 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
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: 7
|
||||
|
||||
NText {
|
||||
text: {
|
||||
// Use the locale's first day of week setting
|
||||
let firstDay = Qt.locale().firstDayOfWeek
|
||||
let dayIndex = (firstDay + index) % 7
|
||||
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
|
||||
}
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: Style.baseWidgetSize * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grids: days
|
||||
MonthGrid {
|
||||
id: grid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true // Take remaining space
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
Layout.rightMargin: Style.marginS * scaling
|
||||
spacing: 0
|
||||
month: Time.date.getMonth()
|
||||
year: Time.date.getFullYear()
|
||||
locale: Qt.locale() // Use system locale
|
||||
|
||||
delegate: Rectangle {
|
||||
width: (Style.baseWidgetSize * scaling)
|
||||
height: (Style.baseWidgetSize * scaling)
|
||||
radius: Style.radiusS * scaling
|
||||
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
|
||||
font.pointSize: (Style.fontSizeM * scaling)
|
||||
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,16 +28,16 @@ NBox {
|
||||
|
||||
NIcon {
|
||||
icon: "disc"
|
||||
font.pointSize: Style.fontSizeXXXL * 3 * scaling
|
||||
pointSize: Style.fontSizeXXXL * 3 * scaling
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NText {
|
||||
text: "No media player detected"
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
// NText {
|
||||
// text: I18n.tr("system.no-media-player-detected")
|
||||
// color: Color.mOnSurfaceVariant
|
||||
// Layout.alignment: Qt.AlignHCenter
|
||||
// }
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
@@ -51,91 +51,71 @@ NBox {
|
||||
visible: MediaService.currentPlayer && MediaService.canPlay
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Player selector
|
||||
ComboBox {
|
||||
id: playerSelector
|
||||
// Player selector using NContextMenu
|
||||
Rectangle {
|
||||
id: playerSelectorButton
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.barHeight * 0.83 * scaling
|
||||
Layout.preferredHeight: Style.barHeight * scaling
|
||||
visible: MediaService.getAvailablePlayers().length > 1
|
||||
model: MediaService.getAvailablePlayers()
|
||||
textRole: "identity"
|
||||
currentIndex: MediaService.selectedPlayerIndex
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.transparent
|
||||
|
||||
background: Rectangle {
|
||||
visible: false
|
||||
// implicitWidth: 120 * scaling
|
||||
// implicitHeight: 30 * scaling
|
||||
color: Color.transparent
|
||||
border.color: playerSelector.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
}
|
||||
property var currentPlayer: MediaService.getAvailablePlayers()[MediaService.selectedPlayerIndex]
|
||||
|
||||
contentItem: NText {
|
||||
visible: false
|
||||
leftPadding: Style.marginM * scaling
|
||||
rightPadding: playerSelector.indicator.width + playerSelector.spacing
|
||||
text: playerSelector.displayText
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
indicator: NIcon {
|
||||
x: playerSelector.width - width
|
||||
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
|
||||
icon: "caret-down"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
id: popup
|
||||
x: playerSelector.width * 0.5
|
||||
y: playerSelector.height * 0.75
|
||||
width: playerSelector.width * 0.5
|
||||
implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginM * scaling)
|
||||
padding: Style.marginS * scaling
|
||||
|
||||
contentItem: ListView {
|
||||
clip: true
|
||||
implicitHeight: contentHeight
|
||||
model: playerSelector.popup.visible ? playerSelector.delegateModel : null
|
||||
currentIndex: playerSelector.highlightedIndex
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
NIcon {
|
||||
icon: "caret-down"
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusXS * scaling
|
||||
NText {
|
||||
text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: playerSelector.width
|
||||
contentItem: NText {
|
||||
text: modelData.identity
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: highlighted ? Color.mSurface : Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
highlighted: playerSelector.highlightedIndex === index
|
||||
MouseArea {
|
||||
id: playerSelectorMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
background: Rectangle {
|
||||
width: popup.width - Style.marginS * scaling * 2
|
||||
color: highlighted ? Color.mSecondary : Color.transparent
|
||||
radius: Style.radiusXS * scaling
|
||||
onClicked: {
|
||||
// Create menu items from available players
|
||||
var menuItems = []
|
||||
var players = MediaService.getAvailablePlayers()
|
||||
for (var i = 0; i < players.length; i++) {
|
||||
menuItems.push({
|
||||
"label": players[i].identity,
|
||||
"action": i.toString(),
|
||||
"icon": "disc",
|
||||
"enabled": true,
|
||||
"visible": true
|
||||
})
|
||||
}
|
||||
playerContextMenu.model = menuItems
|
||||
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height)
|
||||
}
|
||||
}
|
||||
|
||||
onActivated: {
|
||||
MediaService.selectedPlayerIndex = currentIndex
|
||||
MediaService.updateCurrentPlayer()
|
||||
NContextMenu {
|
||||
id: playerContextMenu
|
||||
parent: root
|
||||
width: 200 * scaling
|
||||
|
||||
onTriggered: function (action) {
|
||||
var index = parseInt(action)
|
||||
if (!isNaN(index)) {
|
||||
MediaService.selectedPlayerIndex = index
|
||||
MediaService.updateCurrentPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +147,7 @@ NBox {
|
||||
NIcon {
|
||||
icon: "disc"
|
||||
color: Color.mPrimary
|
||||
font.pointSize: Style.fontSizeXXXL * 3 * scaling
|
||||
pointSize: Style.fontSizeXXXL * 3 * scaling
|
||||
visible: !trackArt.visible
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
@@ -182,7 +162,7 @@ NBox {
|
||||
NText {
|
||||
visible: MediaService.trackTitle !== ""
|
||||
text: MediaService.trackTitle
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
@@ -193,8 +173,8 @@ NBox {
|
||||
NText {
|
||||
visible: MediaService.trackArtist !== ""
|
||||
text: MediaService.trackArtist
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mPrimary
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -203,7 +183,7 @@ NBox {
|
||||
visible: MediaService.trackAlbum !== ""
|
||||
text: MediaService.trackAlbum
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -300,7 +280,7 @@ NBox {
|
||||
// Previous button
|
||||
NIconButton {
|
||||
icon: "media-prev"
|
||||
tooltipText: "Previous Media"
|
||||
tooltipText: I18n.tr("tooltips.previous-media")
|
||||
visible: MediaService.canGoPrevious
|
||||
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
|
||||
}
|
||||
@@ -308,7 +288,7 @@ NBox {
|
||||
// Play/Pause button
|
||||
NIconButton {
|
||||
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
tooltipText: MediaService.isPlaying ? "Pause" : "Play"
|
||||
tooltipText: MediaService.isPlaying ? I18n.tr("tooltips.pause") : I18n.tr("tooltips.play")
|
||||
visible: (MediaService.canPlay || MediaService.canPause)
|
||||
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
|
||||
}
|
||||
@@ -316,7 +296,7 @@ NBox {
|
||||
// Next button
|
||||
NIconButton {
|
||||
icon: "media-next"
|
||||
tooltipText: "Next media"
|
||||
tooltipText: I18n.tr("tooltips.next-media")
|
||||
visible: MediaService.canGoNext
|
||||
onClicked: MediaService.canGoNext ? MediaService.next() : {}
|
||||
}
|
||||
@@ -324,7 +304,7 @@ NBox {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Settings.data.audio.visualizerType == "linear" && MediaService.isPlaying
|
||||
active: Settings.data.audio.visualizerType == "linear"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceComponent: LinearSpectrum {
|
||||
@@ -337,7 +317,7 @@ NBox {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Settings.data.audio.visualizerType == "mirrored" && MediaService.isPlaying
|
||||
active: Settings.data.audio.visualizerType == "mirrored"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceComponent: MirroredSpectrum {
|
||||
@@ -350,7 +330,7 @@ NBox {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Settings.data.audio.visualizerType == "wave" && MediaService.isPlaying
|
||||
active: Settings.data.audio.visualizerType == "wave"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
@@ -25,45 +25,39 @@ NBox {
|
||||
}
|
||||
// Performance
|
||||
NIconButton {
|
||||
icon: "performance"
|
||||
tooltipText: "Set performance power profile."
|
||||
icon: PowerProfileService.getIcon(PowerProfile.Performance)
|
||||
tooltipText: I18n.tr("tooltips.set-power-profile", {
|
||||
"profile": PowerProfileService.getName(PowerProfile.Performance)
|
||||
})
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
PowerProfileService.setProfile(PowerProfile.Performance)
|
||||
}
|
||||
}
|
||||
onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
|
||||
}
|
||||
// Balanced
|
||||
NIconButton {
|
||||
icon: "balanced"
|
||||
tooltipText: "Set balanced power profile."
|
||||
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
|
||||
tooltipText: I18n.tr("tooltips.set-power-profile", {
|
||||
"profile": PowerProfileService.getName(PowerProfile.Balanced)
|
||||
})
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
PowerProfileService.setProfile(PowerProfile.Balanced)
|
||||
}
|
||||
}
|
||||
onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
|
||||
}
|
||||
// Eco
|
||||
NIconButton {
|
||||
icon: "powersaver"
|
||||
tooltipText: "Set eco power profile."
|
||||
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
|
||||
tooltipText: I18n.tr("tooltips.set-power-profile", {
|
||||
"profile": PowerProfileService.getName(PowerProfile.PowerSaver)
|
||||
})
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
PowerProfileService.setProfile(PowerProfile.PowerSaver)
|
||||
}
|
||||
}
|
||||
onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
@@ -4,8 +4,8 @@ import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.SidePanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -42,8 +42,10 @@ NBox {
|
||||
font.capitalization: Font.Capitalize
|
||||
}
|
||||
NText {
|
||||
text: `System uptime: ${uptimeText}`
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
text: I18n.tr("system.uptime", {
|
||||
"uptime": uptimeText
|
||||
})
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
@@ -56,7 +58,7 @@ NBox {
|
||||
}
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: "Open settings."
|
||||
tooltipText: I18n.tr("tooltips.open-settings")
|
||||
onClicked: {
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.General
|
||||
settingsPanel.open()
|
||||
@@ -66,19 +68,19 @@ NBox {
|
||||
NIconButton {
|
||||
id: powerButton
|
||||
icon: "power"
|
||||
tooltipText: "Power menu."
|
||||
tooltipText: I18n.tr("tooltips.session-menu")
|
||||
onClicked: {
|
||||
powerPanel.open()
|
||||
sidePanel.close()
|
||||
sessionMenuPanel.open()
|
||||
controlCenterPanel.close()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
id: closeButton
|
||||
icon: "close"
|
||||
tooltipText: "Close side panel."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
onClicked: {
|
||||
sidePanel.close()
|
||||
controlCenterPanel.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -24,7 +24,7 @@ NBox {
|
||||
NIconButton {
|
||||
icon: "camera-video"
|
||||
enabled: ScreenRecorderService.isAvailable
|
||||
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording.") : "Screen recorder not installed."
|
||||
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? I18n.tr("tooltips.stop-screen-recording") : I18n.tr("tooltips.start-screen-recording")) : I18n.tr("tooltips.screen-recorder-not-installed")
|
||||
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
@@ -33,8 +33,8 @@ NBox {
|
||||
ScreenRecorderService.toggleRecording()
|
||||
// If we were not recording and we just initiated a start, close the panel
|
||||
if (!ScreenRecorderService.isRecording) {
|
||||
var panel = PanelService.getPanel("sidePanel")
|
||||
panel && panel.close()
|
||||
var panel = PanelService.getPanel("controlCenterPanel")
|
||||
panel?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ NBox {
|
||||
// Idle Inhibitor
|
||||
NIconButton {
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake."
|
||||
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
|
||||
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
@@ -54,15 +54,9 @@ NBox {
|
||||
NIconButton {
|
||||
visible: Settings.data.wallpaper.enabled
|
||||
icon: "wallpaper-selector"
|
||||
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
|
||||
onClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
||||
settingsPanel.open()
|
||||
}
|
||||
onRightClicked: {
|
||||
WallpaperService.setRandomWallpaper()
|
||||
}
|
||||
tooltipText: I18n.tr("tooltips.wallpaper-selector")
|
||||
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
|
||||
onRightClicked: WallpaperService.setRandomWallpaper()
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -24,7 +24,7 @@ NBox {
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : ""
|
||||
font.pointSize: Style.fontSizeXXXL * 1.75 * scaling
|
||||
pointSize: Style.fontSizeXXXL * 1.75 * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ NBox {
|
||||
const chunks = Settings.data.location.name.split(",")
|
||||
return chunks[0]
|
||||
}
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
@@ -56,13 +56,13 @@ NBox {
|
||||
temp = Math.round(temp)
|
||||
return `${temp}°${suffix}`
|
||||
}
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
pointSize: Style.fontSizeXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
NText {
|
||||
text: weatherReady ? `(${LocationService.data.weather.timezone_abbreviation})` : ""
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
visible: LocationService.data.weather
|
||||
}
|
||||
@@ -88,7 +88,7 @@ NBox {
|
||||
NText {
|
||||
text: {
|
||||
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
|
||||
return Qt.formatDateTime(weatherDate, "ddd")
|
||||
return Qt.locale().toString(weatherDate, "ddd")
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@@ -96,7 +96,7 @@ NBox {
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
|
||||
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
|
||||
font.pointSize: Style.fontSizeXXL * 1.6 * scaling
|
||||
pointSize: Style.fontSizeXXL * 1.6 * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
NText {
|
||||
@@ -112,7 +112,7 @@ NBox {
|
||||
min = Math.round(min)
|
||||
return `${max}°/${min}°`
|
||||
}
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Modules.SidePanel.Cards
|
||||
import qs.Modules.ControlCenter.Cards
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -13,8 +13,20 @@ Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
property bool barIsReady: BarService.isBarReady(modelData.name)
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onBarReadyChanged(screenName) {
|
||||
if (screenName === modelData.name) {
|
||||
barIsReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
@@ -25,6 +37,32 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when toplevels change
|
||||
Connections {
|
||||
target: ToplevelManager ? ToplevelManager.toplevels : null
|
||||
function onValuesChanged() {
|
||||
updateDockApps()
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when pinned apps change
|
||||
Connections {
|
||||
target: Settings.data.dock
|
||||
function onPinnedAppsChanged() {
|
||||
updateDockApps()
|
||||
}
|
||||
function onOnlySameOutputChanged() {
|
||||
updateDockApps()
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update when component is ready
|
||||
Component.onCompleted: {
|
||||
if (ToplevelManager) {
|
||||
updateDockApps()
|
||||
}
|
||||
}
|
||||
|
||||
// Shared properties between peek and dock windows
|
||||
readonly property bool autoHide: Settings.data.dock.autoHide
|
||||
readonly property int hideDelay: 500
|
||||
@@ -43,12 +81,66 @@ Variants {
|
||||
// Shared state between windows
|
||||
property bool dockHovered: false
|
||||
property bool anyAppHovered: false
|
||||
property bool menuHovered: false
|
||||
property bool hidden: autoHide
|
||||
property bool peekHovered: false
|
||||
|
||||
// Separate property to control Loader - stays true during animations
|
||||
property bool dockLoaded: !autoHide // Start loaded if autoHide is off
|
||||
|
||||
// Track the currently open context menu
|
||||
property var currentContextMenu: null
|
||||
|
||||
// Combined model of running apps and pinned apps
|
||||
property var dockApps: []
|
||||
|
||||
// Function to close any open context menu
|
||||
function closeAllContextMenus() {
|
||||
if (currentContextMenu && currentContextMenu.visible) {
|
||||
currentContextMenu.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update the combined dock apps model
|
||||
function updateDockApps() {
|
||||
const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : []
|
||||
const pinnedApps = Settings.data.dock.pinnedApps || []
|
||||
const combined = []
|
||||
const processedAppIds = new Set()
|
||||
|
||||
// Strategy: Maintain app positions as much as possible
|
||||
// 1. First pass: Add all running apps (both pinned and non-pinned) in their current order
|
||||
runningApps.forEach(toplevel => {
|
||||
if (toplevel && toplevel.appId && !(Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(modelData))) {
|
||||
const isPinned = pinnedApps.includes(toplevel.appId)
|
||||
const appType = isPinned ? "pinned-running" : "running"
|
||||
|
||||
combined.push({
|
||||
"type": appType,
|
||||
"toplevel": toplevel,
|
||||
"appId": toplevel.appId,
|
||||
"title": toplevel.title
|
||||
})
|
||||
processedAppIds.add(toplevel.appId)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Second pass: Add non-running pinned apps at the end
|
||||
pinnedApps.forEach(pinnedAppId => {
|
||||
if (!processedAppIds.has(pinnedAppId)) {
|
||||
// Pinned app that is not running
|
||||
combined.push({
|
||||
"type": "pinned",
|
||||
"toplevel": null,
|
||||
"appId": pinnedAppId,
|
||||
"title": pinnedAppId
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
dockApps = combined
|
||||
}
|
||||
|
||||
// Timer to unload dock after hide animation completes
|
||||
Timer {
|
||||
id: unloadTimer
|
||||
@@ -65,7 +157,7 @@ Variants {
|
||||
id: hideTimer
|
||||
interval: hideDelay
|
||||
onTriggered: {
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
|
||||
hidden = true
|
||||
unloadTimer.restart() // Start unload timer when hiding
|
||||
}
|
||||
@@ -101,7 +193,7 @@ Variants {
|
||||
|
||||
// PEEK WINDOW - Always visible when auto-hide is enabled
|
||||
Loader {
|
||||
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide
|
||||
active: (barIsReady || !hasBar) && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: peekWindow
|
||||
@@ -137,7 +229,7 @@ Variants {
|
||||
|
||||
onExited: {
|
||||
peekHovered = false
|
||||
if (!hidden && !dockHovered && !anyAppHovered) {
|
||||
if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
@@ -147,7 +239,7 @@ Variants {
|
||||
|
||||
// DOCK WINDOW
|
||||
Loader {
|
||||
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0)
|
||||
active: (barIsReady || !hasBar) && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (dockApps.length > 0)
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: dockWindow
|
||||
@@ -235,10 +327,15 @@ Variants {
|
||||
|
||||
onExited: {
|
||||
dockHovered = false
|
||||
if (autoHide && !anyAppHovered && !peekHovered) {
|
||||
if (autoHide && !anyAppHovered && !peekHovered && !menuHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
// Close any open context menu when clicking on the dock background
|
||||
closeAllContextMenus()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -247,10 +344,10 @@ Variants {
|
||||
height: parent.height - (Style.marginM * 2 * scaling)
|
||||
anchors.centerIn: parent
|
||||
|
||||
function getAppIcon(toplevel: Toplevel): string {
|
||||
if (!toplevel)
|
||||
function getAppIcon(appData): string {
|
||||
if (!appData || !appData.appId)
|
||||
return ""
|
||||
return AppIcons.iconForAppId(toplevel.appId?.toLowerCase())
|
||||
return ThemeIcons.iconForAppId(appData.appId?.toLowerCase())
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -260,7 +357,7 @@ Variants {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Repeater {
|
||||
model: ToplevelManager ? ToplevelManager.toplevels : null
|
||||
model: dockApps
|
||||
|
||||
delegate: Item {
|
||||
id: appButton
|
||||
@@ -268,17 +365,18 @@ Variants {
|
||||
Layout.preferredHeight: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
|
||||
property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel
|
||||
property bool hovered: appMouseArea.containsMouse
|
||||
property string appId: modelData ? modelData.appId : ""
|
||||
property string appTitle: modelData ? modelData.title : ""
|
||||
property string appTitle: modelData ? (modelData.title || modelData.appId) : ""
|
||||
property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running")
|
||||
|
||||
// Individual tooltip for this app
|
||||
NTooltip {
|
||||
id: appTooltip
|
||||
target: appButton
|
||||
positionAbove: true
|
||||
visible: false
|
||||
// Listen for the toplevel being closed
|
||||
Connections {
|
||||
target: modelData?.toplevel
|
||||
function onClosed() {
|
||||
Qt.callLater(root.updateDockApps)
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
@@ -296,6 +394,9 @@ Variants {
|
||||
fillMode: Image.PreserveAspectFit
|
||||
cache: true
|
||||
|
||||
// Dim pinned apps that aren't running
|
||||
opacity: appButton.isRunning ? 1.0 : 0.6
|
||||
|
||||
scale: appButton.hovered ? 1.15 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
@@ -305,6 +406,13 @@ Variants {
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back if no icon
|
||||
@@ -312,8 +420,9 @@ Variants {
|
||||
anchors.centerIn: parent
|
||||
visible: !appIcon.visible
|
||||
icon: "question-mark"
|
||||
font.pointSize: iconSize * 0.7
|
||||
pointSize: iconSize * 0.7
|
||||
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
opacity: appButton.isRunning ? 1.0 : 0.6
|
||||
scale: appButton.hovered ? 1.15 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
@@ -323,6 +432,41 @@ Variants {
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu popup
|
||||
DockMenu {
|
||||
id: contextMenu
|
||||
scaling: root.scaling
|
||||
onHoveredChanged: menuHovered = hovered
|
||||
onRequestClose: {
|
||||
contextMenu.hide()
|
||||
// Restart hide timer after menu action if auto-hide is enabled
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
onAppClosed: root.updateDockApps // Force immediate dock update when app is closed
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
root.currentContextMenu = contextMenu
|
||||
} else if (root.currentContextMenu === contextMenu) {
|
||||
root.currentContextMenu = null
|
||||
// Reset menu hover state when menu becomes invisible
|
||||
menuHovered = false
|
||||
// Restart hide timer if conditions are met
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -330,13 +474,13 @@ Variants {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
|
||||
onEntered: {
|
||||
anyAppHovered = true
|
||||
const appName = appButton.appTitle || appButton.appId || "Unknown"
|
||||
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
|
||||
appTooltip.isVisible = true
|
||||
const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
|
||||
TooltipService.show(appButton, tooltipText, "top")
|
||||
if (autoHide) {
|
||||
showTimer.stop()
|
||||
hideTimer.stop()
|
||||
@@ -346,18 +490,44 @@ Variants {
|
||||
|
||||
onExited: {
|
||||
anyAppHovered = false
|
||||
appTooltip.hide()
|
||||
if (autoHide && !dockHovered && !peekHovered) {
|
||||
TooltipService.hide()
|
||||
if (autoHide && !dockHovered && !peekHovered && !menuHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.MiddleButton && modelData?.close) {
|
||||
modelData.close()
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
// If right-clicking on the same app with an open context menu, close it
|
||||
if (root.currentContextMenu === contextMenu && contextMenu.visible) {
|
||||
root.closeAllContextMenus()
|
||||
return
|
||||
}
|
||||
// Close any other existing context menu first
|
||||
root.closeAllContextMenus()
|
||||
// Hide tooltip when showing context menu
|
||||
TooltipService.hide()
|
||||
contextMenu.show(appButton, modelData.toplevel || modelData)
|
||||
return
|
||||
}
|
||||
if (mouse.button === Qt.LeftButton && modelData?.activate) {
|
||||
modelData.activate()
|
||||
|
||||
// Close any existing context menu for non-right-click actions
|
||||
root.closeAllContextMenus()
|
||||
|
||||
// Check if toplevel is still valid (not a stale reference)
|
||||
const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel)
|
||||
|
||||
if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) {
|
||||
modelData.toplevel.close()
|
||||
Qt.callLater(root.updateDockApps) // Force immediate dock update
|
||||
} else if (mouse.button === Qt.LeftButton) {
|
||||
if (isValidToplevel && modelData.toplevel.activate) {
|
||||
// Running app - activate it
|
||||
modelData.toplevel.activate()
|
||||
} else if (modelData?.appId) {
|
||||
// Pinned app not running - launch it
|
||||
Quickshell.execDetached(["gtk-launch", modelData.appId])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
273
Modules/Dock/DockMenu.qml
Normal file
273
Modules/Dock/DockMenu.qml
Normal file
@@ -0,0 +1,273 @@
|
||||
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
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
property var toplevel: null
|
||||
property Item anchorItem: null
|
||||
property real scaling: 1.0
|
||||
property bool hovered: menuMouseArea.containsMouse
|
||||
property var onAppClosed: null // Callback function for when an app is closed
|
||||
|
||||
// Track which menu item is hovered
|
||||
property int hoveredItem: -1 // -1: none, 0: focus, 1: pin, 2: close
|
||||
|
||||
signal requestClose
|
||||
|
||||
implicitWidth: 140 * scaling
|
||||
implicitHeight: contextMenuColumn.implicitHeight + (Style.marginM * scaling * 2)
|
||||
color: Color.transparent
|
||||
visible: false
|
||||
|
||||
// Helper functions for pin/unpin functionality
|
||||
function isAppPinned(appId) {
|
||||
if (!appId)
|
||||
return false
|
||||
const pinnedApps = Settings.data.dock.pinnedApps || []
|
||||
return pinnedApps.includes(appId)
|
||||
}
|
||||
|
||||
function toggleAppPin(appId) {
|
||||
if (!appId)
|
||||
return
|
||||
|
||||
let pinnedApps = (Settings.data.dock.pinnedApps || []).slice() // Create a copy
|
||||
const isPinned = pinnedApps.includes(appId)
|
||||
|
||||
if (isPinned) {
|
||||
// Unpin: remove from array
|
||||
pinnedApps = pinnedApps.filter(id => id !== appId)
|
||||
} else {
|
||||
// Pin: add to array
|
||||
pinnedApps.push(appId)
|
||||
}
|
||||
|
||||
// Update the settings
|
||||
Settings.data.dock.pinnedApps = pinnedApps
|
||||
}
|
||||
|
||||
anchor.item: anchorItem
|
||||
anchor.rect.x: anchorItem ? (anchorItem.width - implicitWidth) / 2 : 0
|
||||
anchor.rect.y: anchorItem ? -implicitHeight - (Style.marginM * scaling) : 0
|
||||
|
||||
function show(item, toplevelData) {
|
||||
if (!item) {
|
||||
Logger.warn("DockMenu", "anchorItem is undefined, won't show menu.")
|
||||
return
|
||||
}
|
||||
|
||||
anchorItem = item
|
||||
toplevel = toplevelData
|
||||
visible = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible = false
|
||||
}
|
||||
|
||||
// Helper function to determine which menu item is under the mouse
|
||||
function getHoveredItem(mouseY) {
|
||||
const itemHeight = 32 * scaling
|
||||
const startY = Style.marginM * scaling
|
||||
const relativeY = mouseY - startY
|
||||
|
||||
if (relativeY < 0)
|
||||
return -1
|
||||
|
||||
const itemIndex = Math.floor(relativeY / itemHeight)
|
||||
return itemIndex >= 0 && itemIndex < 3 ? itemIndex : -1
|
||||
}
|
||||
|
||||
// Handle menu item clicks
|
||||
function handleItemClick(itemIndex) {
|
||||
switch (itemIndex) {
|
||||
case 0:
|
||||
// Focus
|
||||
if (root.toplevel?.activate) {
|
||||
root.toplevel.activate()
|
||||
}
|
||||
root.requestClose()
|
||||
break
|
||||
case 1:
|
||||
// Pin/Unpin
|
||||
if (root.toplevel?.appId) {
|
||||
root.toggleAppPin(root.toplevel.appId)
|
||||
}
|
||||
root.requestClose()
|
||||
break
|
||||
case 2:
|
||||
// Close
|
||||
// Check if toplevel is still valid before trying to close it
|
||||
const isValidToplevel = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel)
|
||||
|
||||
if (isValidToplevel && root.toplevel.close) {
|
||||
root.toplevel.close()
|
||||
// Trigger immediate dock update callback if provided
|
||||
if (root.onAppClosed && typeof root.onAppClosed === "function") {
|
||||
Qt.callLater(root.onAppClosed)
|
||||
}
|
||||
} else {
|
||||
Logger.warn("DockMenu", "Cannot close app - invalid toplevel reference")
|
||||
}
|
||||
root.hide()
|
||||
root.requestClose()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusS * scaling
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Single MouseArea to handle both auto-close and menu interactions
|
||||
MouseArea {
|
||||
id: menuMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.hoveredItem >= 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
onEntered: {
|
||||
closeTimer.stop()
|
||||
}
|
||||
|
||||
onExited: {
|
||||
root.hoveredItem = -1
|
||||
closeTimer.start()
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
root.hoveredItem = root.getHoveredItem(mouse.y)
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
const clickedItem = root.getHoveredItem(mouse.y)
|
||||
if (clickedItem >= 0) {
|
||||
root.handleItemClick(clickedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contextMenuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: 0
|
||||
|
||||
// Focus item
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 32 * scaling
|
||||
color: root.hoveredItem === 0 ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusXS * scaling
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "eye"
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: root.hoveredItem === 0 ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("dock.menu.focus")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: root.hoveredItem === 0 ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pin/Unpin item
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 32 * scaling
|
||||
color: root.hoveredItem === 1 ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusXS * scaling
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: {
|
||||
if (!root.toplevel)
|
||||
return "pin"
|
||||
return root.isAppPinned(root.toplevel.appId) ? "unpin" : "pin"
|
||||
}
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: root.hoveredItem === 1 ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (!root.toplevel)
|
||||
return I18n.tr("dock.menu.pin")
|
||||
return root.isAppPinned(root.toplevel.appId) ? I18n.tr("dock.menu.unpin") : I18n.tr("dock.menu.pin")
|
||||
}
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: root.hoveredItem === 1 ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close item
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 32 * scaling
|
||||
color: root.hoveredItem === 2 ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusXS * scaling
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "close"
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: root.hoveredItem === 2 ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("dock.menu.close")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: root.hoveredItem === 2 ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ NPanel {
|
||||
fontWeight: Style.fontWeightSemiBold
|
||||
|
||||
text: searchText
|
||||
placeholderText: "Search entries... or use > for commands"
|
||||
placeholderText: I18n.tr("placeholders.search-launcher")
|
||||
|
||||
onTextChanged: searchText = text
|
||||
|
||||
@@ -302,11 +302,34 @@ NPanel {
|
||||
positionViewAtIndex(currentIndex, ListView.Contain)
|
||||
}
|
||||
}
|
||||
onModelChanged: {
|
||||
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
|
||||
property bool isSelected: mouseArea.containsMouse || (index === selectedIndex)
|
||||
// Accessor for app id
|
||||
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
|
||||
|
||||
// Pin helpers
|
||||
function togglePin(appId) {
|
||||
if (!appId)
|
||||
return
|
||||
let arr = (Settings.data.dock.pinnedApps || []).slice()
|
||||
const idx = arr.indexOf(appId)
|
||||
if (idx >= 0)
|
||||
arr.splice(idx, 1)
|
||||
else
|
||||
arr.push(appId)
|
||||
Settings.data.dock.pinnedApps = arr
|
||||
}
|
||||
|
||||
function isPinned(appId) {
|
||||
const arr = Settings.data.dock.pinnedApps || []
|
||||
return appId && arr.indexOf(appId) >= 0
|
||||
}
|
||||
|
||||
// Property to reliably track the current item's ID.
|
||||
// This changes whenever the delegate is recycled for a new item.
|
||||
@@ -321,7 +344,7 @@ NPanel {
|
||||
}
|
||||
|
||||
width: resultsList.width - Style.marginS * scaling
|
||||
height: entryHeight
|
||||
implicitHeight: entryHeight
|
||||
radius: Style.radiusM * scaling
|
||||
color: entry.isSelected ? Color.mTertiary : Color.mSurface
|
||||
|
||||
@@ -332,136 +355,152 @@ NPanel {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Icon badge or Image preview
|
||||
Rectangle {
|
||||
Layout.preferredWidth: badgeSize
|
||||
Layout.preferredHeight: badgeSize
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
clip: true
|
||||
// Top row - Main entry content with pin button
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Image preview for clipboard images
|
||||
NImageRounded {
|
||||
id: imagePreview
|
||||
anchors.fill: parent
|
||||
visible: modelData.isImage
|
||||
imageRadius: Style.radiusM * scaling
|
||||
|
||||
// This property creates a dependency on the service's revision counter
|
||||
readonly property int _rev: ClipboardService.revision
|
||||
|
||||
// Fetches from the service's cache.
|
||||
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
|
||||
imagePath: {
|
||||
_rev
|
||||
return ClipboardService.getImageData(modelData.clipboardId) || ""
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: true
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: width
|
||||
}
|
||||
}
|
||||
|
||||
// Error fallback
|
||||
onStatusChanged: status => {
|
||||
if (status === Image.Error) {
|
||||
iconLoader.visible = true
|
||||
imagePreview.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icon fallback
|
||||
Loader {
|
||||
id: iconLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS * scaling
|
||||
|
||||
visible: !modelData.isImage || imagePreview.status === Image.Error
|
||||
active: visible
|
||||
|
||||
sourceComponent: Component {
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
|
||||
visible: modelData.icon && source !== ""
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback text if no icon and no image
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
visible: !imagePreview.visible && !iconLoader.visible
|
||||
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
|
||||
// Image type indicator overlay
|
||||
// Icon badge or Image preview
|
||||
Rectangle {
|
||||
visible: modelData.isImage && imagePreview.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2 * scaling
|
||||
width: formatLabel.width + 6 * scaling
|
||||
height: formatLabel.height + 2 * scaling
|
||||
Layout.preferredWidth: badgeSize
|
||||
Layout.preferredHeight: badgeSize
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
clip: true
|
||||
|
||||
NText {
|
||||
id: formatLabel
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (!modelData.isImage)
|
||||
return ""
|
||||
const desc = modelData.description || ""
|
||||
const parts = desc.split(" • ")
|
||||
return parts[0] || "IMG"
|
||||
// Image preview for clipboard images
|
||||
NImageRounded {
|
||||
id: imagePreview
|
||||
anchors.fill: parent
|
||||
visible: modelData.isImage
|
||||
imageRadius: Style.radiusM * scaling
|
||||
|
||||
// This property creates a dependency on the service's revision counter
|
||||
readonly property int _rev: ClipboardService.revision
|
||||
|
||||
// Fetches from the service's cache.
|
||||
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
|
||||
imagePath: {
|
||||
_rev
|
||||
return ClipboardService.getImageData(modelData.clipboardId) || ""
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: true
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: width
|
||||
}
|
||||
}
|
||||
|
||||
// Error fallback
|
||||
onStatusChanged: status => {
|
||||
if (status === Image.Error) {
|
||||
iconLoader.visible = true
|
||||
imagePreview.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icon fallback
|
||||
Loader {
|
||||
id: iconLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS * scaling
|
||||
|
||||
visible: !modelData.isImage || imagePreview.status === Image.Error
|
||||
active: visible
|
||||
|
||||
sourceComponent: Component {
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
|
||||
visible: modelData.icon && source !== ""
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback text if no icon and no image
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
visible: !imagePreview.visible && !iconLoader.visible
|
||||
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
|
||||
// Image type indicator overlay
|
||||
Rectangle {
|
||||
visible: modelData.isImage && imagePreview.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2 * scaling
|
||||
width: formatLabel.width + 6 * scaling
|
||||
height: formatLabel.height + 2 * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
NText {
|
||||
id: formatLabel
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (!modelData.isImage)
|
||||
return ""
|
||||
const desc = modelData.description || ""
|
||||
const parts = desc.split(" • ")
|
||||
return parts[0] || "IMG"
|
||||
}
|
||||
pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text content
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0 * scaling
|
||||
|
||||
NText {
|
||||
text: modelData.name || "Unknown"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
// Text content
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0 * scaling
|
||||
|
||||
NText {
|
||||
text: modelData.name || "Unknown"
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.description || ""
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.description || ""
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
visible: text !== ""
|
||||
// Pin/Unpin action icon button
|
||||
NIconButton {
|
||||
visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0)
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
icon: entry.isPinned(entry.appId) ? "unpin" : "pin"
|
||||
tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin")
|
||||
onClicked: entry.togglePin(entry.appId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,12 +508,17 @@ NPanel {
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedIndex = index
|
||||
ui.activate()
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
selectedIndex = index
|
||||
ui.activate()
|
||||
mouse.accepted = true
|
||||
}
|
||||
}
|
||||
acceptedButtons: Qt.LeftButton
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -492,7 +536,7 @@ NPanel {
|
||||
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : ""
|
||||
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`
|
||||
}
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignCenter
|
||||
}
|
||||
|
||||
@@ -7,10 +7,42 @@ import "../../../Helpers/FuzzySort.js" as Fuzzysort
|
||||
|
||||
Item {
|
||||
property var launcher: null
|
||||
property string name: "Applications"
|
||||
property string name: I18n.tr("plugins.applications")
|
||||
property bool handleSearch: true
|
||||
property var entries: []
|
||||
|
||||
// Persistent usage tracking stored in cacheDir
|
||||
property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json"
|
||||
|
||||
// Debounced saver to avoid excessive IO
|
||||
Timer {
|
||||
id: saveTimer
|
||||
interval: 750
|
||||
repeat: false
|
||||
onTriggered: usageFile.writeAdapter()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: usageFile
|
||||
path: usageFilePath
|
||||
printErrors: false
|
||||
watchChanges: false
|
||||
|
||||
onLoadFailed: function (error) {
|
||||
if (error.toString().includes("No such file") || error === 2) {
|
||||
writeAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
onAdapterUpdated: saveTimer.start()
|
||||
|
||||
JsonAdapter {
|
||||
id: usageAdapter
|
||||
// key: app id/command, value: integer count
|
||||
property var counts: ({})
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
loadApplications()
|
||||
}
|
||||
@@ -36,8 +68,32 @@ Item {
|
||||
return []
|
||||
|
||||
if (!query || query.trim() === "") {
|
||||
// Return all apps alphabetically
|
||||
return entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(app => createResultEntry(app))
|
||||
// Return all apps, optionally sorted by usage
|
||||
const favoriteApps = Settings.data.appLauncher.favoriteApps || []
|
||||
let sorted
|
||||
if (Settings.data.appLauncher.sortByMostUsed) {
|
||||
sorted = entries.slice().sort((a, b) => {
|
||||
// Favorites first
|
||||
const aFav = favoriteApps.includes(getAppKey(a))
|
||||
const bFav = favoriteApps.includes(getAppKey(b))
|
||||
if (aFav !== bFav)
|
||||
return aFav ? -1 : 1
|
||||
const ua = getUsageCount(a)
|
||||
const ub = getUsageCount(b)
|
||||
if (ub !== ua)
|
||||
return ub - ua
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase())
|
||||
})
|
||||
} else {
|
||||
sorted = entries.slice().sort((a, b) => {
|
||||
const aFav = favoriteApps.includes(getAppKey(a))
|
||||
const bFav = favoriteApps.includes(getAppKey(b))
|
||||
if (aFav !== bFav)
|
||||
return aFav ? -1 : 1
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase())
|
||||
})
|
||||
}
|
||||
return sorted.map(app => createResultEntry(app))
|
||||
}
|
||||
|
||||
// Use fuzzy search if available, fallback to simple search
|
||||
@@ -48,7 +104,18 @@ Item {
|
||||
"limit": 20
|
||||
})
|
||||
|
||||
return fuzzyResults.map(result => createResultEntry(result.obj))
|
||||
// Sort favorites first within fuzzy results while preserving fuzzysort order otherwise
|
||||
const favoriteApps = Settings.data.appLauncher.favoriteApps || []
|
||||
const fav = []
|
||||
const nonFav = []
|
||||
for (const r of fuzzyResults) {
|
||||
const app = r.obj
|
||||
if (favoriteApps.includes(getAppKey(app)))
|
||||
fav.push(r)
|
||||
else
|
||||
nonFav.push(r)
|
||||
}
|
||||
return fav.concat(nonFav).map(result => createResultEntry(result.obj))
|
||||
} else {
|
||||
// Fallback to simple search
|
||||
const searchTerm = query.toLowerCase()
|
||||
@@ -74,6 +141,7 @@ Item {
|
||||
|
||||
function createResultEntry(app) {
|
||||
return {
|
||||
"appId": getAppKey(app),
|
||||
"name": app.name || "Unknown",
|
||||
"description": app.genericName || app.comment || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
@@ -84,18 +152,60 @@ Item {
|
||||
launcher.closeCompleted()
|
||||
|
||||
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
|
||||
// Record usage and persist asynchronously
|
||||
if (Settings.data.appLauncher.sortByMostUsed)
|
||||
recordUsage(app)
|
||||
if (Settings.data.appLauncher.useApp2Unit && app.id) {
|
||||
Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
|
||||
if (app.runInTerminal)
|
||||
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"])
|
||||
else
|
||||
Quickshell.execDetached(["app2unit", "--"].concat(app.command))
|
||||
} else if (app.execute) {
|
||||
app.execute()
|
||||
} else {
|
||||
Logger.log("ApplicationsPlugin", `Could not launch: ${app.name}`)
|
||||
// Fallback logic when app2unit is not used
|
||||
if (app.runInTerminal) {
|
||||
// If app.execute() fails for terminal apps, we handle it manually.
|
||||
Logger.log("ApplicationsPlugin", "Executing terminal app manually: " + app.name)
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ")
|
||||
const command = terminal.concat(app.command)
|
||||
Quickshell.execDetached(command)
|
||||
} else if (app.execute) {
|
||||
// Default execution for GUI apps
|
||||
app.execute()
|
||||
} else {
|
||||
Logger.log("ApplicationsPlugin", `Could not launch: ${app.name}. No valid launch method.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Usage tracking helpers
|
||||
function getAppKey(app) {
|
||||
if (app && app.id)
|
||||
return String(app.id)
|
||||
if (app && app.command && app.command.join)
|
||||
return app.command.join(" ")
|
||||
return String(app && app.name ? app.name : "unknown")
|
||||
}
|
||||
|
||||
function getUsageCount(app) {
|
||||
const key = getAppKey(app)
|
||||
const m = usageAdapter && usageAdapter.counts ? usageAdapter.counts : null
|
||||
if (!m)
|
||||
return 0
|
||||
const v = m[key]
|
||||
return typeof v === 'number' && isFinite(v) ? v : 0
|
||||
}
|
||||
|
||||
function recordUsage(app) {
|
||||
const key = getAppKey(app)
|
||||
if (!usageAdapter.counts)
|
||||
usageAdapter.counts = ({})
|
||||
const current = getUsageCount(app)
|
||||
usageAdapter.counts[key] = current + 1
|
||||
// Trigger save via debounced timer
|
||||
saveTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
import qs.Commons
|
||||
import "../../../Helpers/AdvancedMath.js" as AdvancedMath
|
||||
|
||||
Item {
|
||||
property var launcher: null
|
||||
property string name: "Calculator"
|
||||
property string name: I18n.tr("plugins.calculator")
|
||||
|
||||
function handleCommand(query) {
|
||||
// Handle >calc command or direct math expressions after >
|
||||
@@ -14,7 +15,7 @@ Item {
|
||||
function commands() {
|
||||
return [{
|
||||
"name": ">calc",
|
||||
"description": "Calculator - evaluate mathematical expressions",
|
||||
"description": I18n.tr("plugins.calculator-description"),
|
||||
"icon": "accessories-calculator",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
@@ -36,8 +37,8 @@ Item {
|
||||
|
||||
if (!expression) {
|
||||
return [{
|
||||
"name": "Calculator",
|
||||
"description": "Enter a mathematical expression",
|
||||
"name": I18n.tr("plugins.calculator-name"),
|
||||
"description": I18n.tr("plugins.calculator-enter-expression"),
|
||||
"icon": "accessories-calculator",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
@@ -59,7 +60,7 @@ Item {
|
||||
}]
|
||||
} catch (error) {
|
||||
return [{
|
||||
"name": "Error",
|
||||
"name": I18n.tr("plugins.calculator-error"),
|
||||
"description": error.message || "Invalid expression",
|
||||
"icon": "dialog-error",
|
||||
"isImage": false,
|
||||
|
||||
@@ -7,7 +7,7 @@ Item {
|
||||
id: root
|
||||
|
||||
// Plugin metadata
|
||||
property string name: "Clipboard History"
|
||||
property string name: I18n.tr("plugins.clipboard")
|
||||
property var launcher: null
|
||||
|
||||
// Plugin capabilities
|
||||
@@ -68,7 +68,7 @@ Item {
|
||||
function commands() {
|
||||
return [{
|
||||
"name": ">clip",
|
||||
"description": "Search clipboard history",
|
||||
"description": I18n.tr("plugins.clipboard-search-description"),
|
||||
"icon": "text-x-generic",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
@@ -76,7 +76,7 @@ Item {
|
||||
}
|
||||
}, {
|
||||
"name": ">clip clear",
|
||||
"description": "Clear all clipboard history",
|
||||
"description": I18n.tr("plugins.clipboard-clear-description"),
|
||||
"icon": "text-x-generic",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
@@ -99,8 +99,8 @@ Item {
|
||||
// Check if clipboard service is not active
|
||||
if (!ClipboardService.active) {
|
||||
return [{
|
||||
"name": "Clipboard History Disabled",
|
||||
"description": "Enable clipboard history in settings or install cliphist",
|
||||
"name": I18n.tr("plugins.clipboard-history-disabled"),
|
||||
"description": I18n.tr("plugins.clipboard-history-disabled-description"),
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
@@ -110,8 +110,8 @@ Item {
|
||||
// Special command: clear
|
||||
if (query === "clear") {
|
||||
return [{
|
||||
"name": "Clear Clipboard History",
|
||||
"description": "Remove all items from clipboard history",
|
||||
"name": I18n.tr("plugins.clipboard-clear-history"),
|
||||
"description": I18n.tr("plugins.clipboard-clear-description-full"),
|
||||
"icon": "delete_sweep",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
@@ -124,8 +124,8 @@ Item {
|
||||
// Show loading state if data is being loaded
|
||||
if (ClipboardService.loading || isWaitingForData) {
|
||||
return [{
|
||||
"name": "Loading clipboard history...",
|
||||
"description": "Please wait",
|
||||
"name": I18n.tr("plugins.clipboard-loading"),
|
||||
"description": I18n.tr("plugins.clipboard-loading-description"),
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
@@ -140,8 +140,8 @@ Item {
|
||||
isWaitingForData = true
|
||||
ClipboardService.list(100)
|
||||
return [{
|
||||
"name": "Loading clipboard history...",
|
||||
"description": "Please wait",
|
||||
"name": I18n.tr("plugins.clipboard-loading"),
|
||||
"description": I18n.tr("plugins.clipboard-loading-description"),
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
|
||||
@@ -26,6 +26,15 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime() {
|
||||
return Settings.data.location.use12hourFormat ? Qt.locale().toString(new Date(), "h:mm A") : Qt.locale().toString(new Date(), "HH:mm")
|
||||
}
|
||||
|
||||
function formatDate() {
|
||||
// For full text date, day is always before month, so we use this format for everybody: Wednesday, September 17.
|
||||
return Qt.locale().toString(new Date(), "dddd, MMMM d")
|
||||
}
|
||||
|
||||
function scheduleUnloadAfterUnlock() {
|
||||
unloadAfterUnlockTimer.start()
|
||||
}
|
||||
@@ -137,9 +146,9 @@ Loader {
|
||||
|
||||
NText {
|
||||
id: timeText
|
||||
text: Qt.formatDateTime(new Date(), "HH:mm")
|
||||
font.family: Settings.data.ui.fontBillboard
|
||||
font.pointSize: Style.fontSizeXXXL * 6 * scaling
|
||||
text: formatTime()
|
||||
// Smaller time display when using longer 12 hour format
|
||||
pointSize: Settings.data.location.use12hourFormat ? Style.fontSizeXXXL * 4 * scaling : Style.fontSizeXXXL * 5 * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
font.letterSpacing: -2 * scaling
|
||||
color: Color.mOnSurface
|
||||
@@ -163,9 +172,8 @@ Loader {
|
||||
|
||||
NText {
|
||||
id: dateText
|
||||
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
|
||||
font.family: Settings.data.ui.fontBillboard
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
text: formatDate()
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Font.Light
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
@@ -189,7 +197,7 @@ Loader {
|
||||
z: 10
|
||||
|
||||
Loader {
|
||||
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear"
|
||||
active: Settings.data.audio.visualizerType == "linear"
|
||||
anchors.centerIn: parent
|
||||
width: 160 * scaling
|
||||
height: 160 * scaling
|
||||
@@ -218,7 +226,7 @@ Loader {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored"
|
||||
active: Settings.data.audio.visualizerType == "mirrored"
|
||||
anchors.centerIn: parent
|
||||
width: 160 * scaling
|
||||
height: 160 * scaling
|
||||
@@ -248,7 +256,7 @@ Loader {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave"
|
||||
active: Settings.data.audio.visualizerType == "wave"
|
||||
anchors.centerIn: parent
|
||||
width: 160 * scaling
|
||||
height: 160 * scaling
|
||||
@@ -295,31 +303,6 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 24 * scaling
|
||||
height: parent.height + 24 * scaling
|
||||
radius: width * 0.5
|
||||
color: Color.transparent
|
||||
border.color: Qt.alpha(Color.mPrimary, 0.3)
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
z: -1
|
||||
visible: !MediaService.isPlaying
|
||||
SequentialAnimation on scale {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.1
|
||||
duration: 1500
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 1500
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NImageCircled {
|
||||
anchors.centerIn: parent
|
||||
width: 100 * scaling
|
||||
@@ -354,6 +337,7 @@ Loader {
|
||||
Rectangle {
|
||||
id: terminalBackground
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
radius: Style.radiusM * scaling
|
||||
color: Qt.alpha(Color.mSurface, 0.9)
|
||||
border.color: Color.mPrimary
|
||||
@@ -397,10 +381,10 @@ Loader {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NText {
|
||||
text: "SECURE TERMINAL"
|
||||
text: I18n.tr("lock-screen.secure-terminal")
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -410,13 +394,13 @@ Loader {
|
||||
NText {
|
||||
text: keyboardLayout.currentLayout
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
NIcon {
|
||||
icon: "keyboard"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
@@ -426,15 +410,15 @@ Loader {
|
||||
visible: batteryIndicator.batteryVisible
|
||||
NIcon {
|
||||
icon: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging, batteryIndicator.isReady)
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
|
||||
rotation: -90
|
||||
}
|
||||
NText {
|
||||
text: Math.round(batteryIndicator.percent) + "%"
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
}
|
||||
@@ -457,8 +441,8 @@ Loader {
|
||||
NText {
|
||||
text: Quickshell.env("USER") + "@noctalia:~$"
|
||||
color: Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
@@ -466,10 +450,12 @@ Loader {
|
||||
id: welcomeText
|
||||
text: ""
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
property int currentIndex: 0
|
||||
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
|
||||
property string fullText: I18n.tr("system.welcome-back", {
|
||||
"user": Quickshell.env("USER")
|
||||
})
|
||||
|
||||
Timer {
|
||||
interval: Style.animationFast
|
||||
@@ -494,16 +480,29 @@ Loader {
|
||||
NText {
|
||||
text: Quickshell.env("USER") + "@noctalia:~$"
|
||||
color: Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "sudo unlock-session"
|
||||
text: I18n.tr("lock-screen.unlock-command")
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: I18n.tr("lock-screen.password")
|
||||
color: Color.mPrimary
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
TextInput {
|
||||
@@ -535,48 +534,60 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: asterisksText
|
||||
text: "*".repeat(passwordInput.text.length)
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
visible: passwordInput.activeFocus && !lockContext.unlockInProgress
|
||||
// Container for asterisks and cursor to control positioning
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: asterisksText.implicitHeight
|
||||
|
||||
SequentialAnimation {
|
||||
id: typingEffect
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.01
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
duration: 50
|
||||
NText {
|
||||
id: asterisksText
|
||||
text: "*".repeat(passwordInput.text.length)
|
||||
color: Color.mOnSurface
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
visible: passwordInput.activeFocus && !lockContext.unlockInProgress
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
|
||||
SequentialAnimation {
|
||||
id: typingEffect
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.01
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
duration: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 8 * scaling
|
||||
height: 20 * scaling
|
||||
color: Color.mPrimary
|
||||
visible: passwordInput.activeFocus
|
||||
Layout.leftMargin: -Style.marginS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Rectangle {
|
||||
width: 8 * scaling
|
||||
height: 20 * scaling
|
||||
color: Color.mPrimary
|
||||
visible: passwordInput.activeFocus
|
||||
anchors.left: asterisksText.right
|
||||
anchors.leftMargin: Style.marginXS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 500
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.0
|
||||
duration: 500
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 500
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.0
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,8 +610,7 @@ Loader {
|
||||
return Color.mError
|
||||
return Color.transparent
|
||||
}
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
@@ -620,6 +630,7 @@ Loader {
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.bottomMargin: -10 * scaling
|
||||
Layout.fillWidth: true
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 120 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
@@ -633,8 +644,8 @@ Loader {
|
||||
anchors.centerIn: parent
|
||||
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
|
||||
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
@@ -708,6 +719,149 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
// ALARMING Easter Egg for long passwords
|
||||
Item {
|
||||
id: easterEggContainer
|
||||
anchors.fill: parent
|
||||
z: 1000
|
||||
|
||||
property bool easterEggTriggered: false
|
||||
|
||||
// Monitor password length
|
||||
Connections {
|
||||
target: passwordInput
|
||||
function onTextChanged() {
|
||||
if (passwordInput.text.length >= 25) {
|
||||
easterEggContainer.easterEggTriggered = true
|
||||
}
|
||||
}
|
||||
function onActiveFocusChanged() {
|
||||
if (!passwordInput.activeFocus) {
|
||||
easterEggContainer.easterEggTriggered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also reset when authentication starts
|
||||
Connections {
|
||||
target: lockContext
|
||||
function onUnlockInProgressChanged() {
|
||||
if (lockContext.unlockInProgress) {
|
||||
easterEggContainer.easterEggTriggered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scattered warning messages (game-style pop-ups)
|
||||
Repeater {
|
||||
model: easterEggContainer.easterEggTriggered && passwordInput.activeFocus && !lockContext.unlockInProgress ? 12 : 0
|
||||
|
||||
NText {
|
||||
property var messages: ["BREACH DETECTED", "SECURITY ALERT", "SYSTEM COMPROMISED", "ANOMALY DETECTED", "FIREWALL BREACH", "DEFENSE FAILING", "16 // 16 // 16", "THE ATLAS SEES ALL", "SIMULATION DETECTED", "WAKE UP", "16 16 16 16 16", "KZZT... 16... KZZT", "ERROR ERROR ERROR", "THEY'RE WATCHING", "16 MINUTES REMAIN"]
|
||||
|
||||
property real baseX: Math.random() * (parent.width - 300)
|
||||
property real baseY: Math.random() * (parent.height - 80)
|
||||
|
||||
text: messages[index % messages.length]
|
||||
color: Color.mError
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
|
||||
x: baseX
|
||||
y: baseY
|
||||
|
||||
// Better random positioning avoiding center terminal
|
||||
Component.onCompleted: {
|
||||
var centerX = parent.width / 2
|
||||
var centerY = parent.height / 2
|
||||
var avoidRadius = 350 * scaling
|
||||
|
||||
// If too close to center, push to random edge zones
|
||||
var distanceFromCenter = Math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY))
|
||||
if (distanceFromCenter < avoidRadius) {
|
||||
// Pick a random edge zone
|
||||
var zone = Math.floor(Math.random() * 4)
|
||||
switch (zone) {
|
||||
case 0:
|
||||
// Top
|
||||
x = Math.random() * parent.width
|
||||
y = Math.random() * 100 * scaling
|
||||
break
|
||||
case 1:
|
||||
// Right
|
||||
x = parent.width - (50 + Math.random() * 200) * scaling
|
||||
y = Math.random() * parent.height
|
||||
break
|
||||
case 2:
|
||||
// Bottom
|
||||
x = Math.random() * parent.width
|
||||
y = parent.height - (50 + Math.random() * 100) * scaling
|
||||
break
|
||||
case 3:
|
||||
// Left
|
||||
x = Math.random() * 200 * scaling
|
||||
y = Math.random() * parent.height
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add some random drift to make positioning more varied
|
||||
x += (Math.random() - 0.5) * 100 * scaling
|
||||
y += (Math.random() - 0.5) * 50 * scaling
|
||||
|
||||
// Ensure we stay within bounds
|
||||
x = Math.max(20 * scaling, Math.min(parent.width - 280 * scaling, x))
|
||||
y = Math.max(20 * scaling, Math.min(parent.height - 60 * scaling, y))
|
||||
}
|
||||
|
||||
// Simple pop-in animation
|
||||
SequentialAnimation on scale {
|
||||
loops: Animation.Infinite
|
||||
PauseAnimation {
|
||||
duration: index * 400 + Math.random() * 1000
|
||||
}
|
||||
NumberAnimation {
|
||||
from: 0
|
||||
to: 1.2
|
||||
duration: 300
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 200
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: 2000 + Math.random() * 3000
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0
|
||||
duration: 300
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: 800 + Math.random() * 1200
|
||||
}
|
||||
}
|
||||
|
||||
// Gentle blinking effect
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
PauseAnimation {
|
||||
duration: index * 200
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.6
|
||||
duration: 400 + Math.random() * 300
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 300 + Math.random() * 200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power buttons at bottom right
|
||||
RowLayout {
|
||||
anchors.right: parent.right
|
||||
@@ -728,7 +882,7 @@ Loader {
|
||||
id: iconPower
|
||||
anchors.centerIn: parent
|
||||
icon: "shutdown"
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
pointSize: Style.fontSizeXXXL * scaling
|
||||
color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError
|
||||
}
|
||||
|
||||
@@ -736,8 +890,8 @@ Loader {
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: 12 * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
anchors.bottomMargin: Style.marginM * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
@@ -747,8 +901,9 @@ Loader {
|
||||
id: shutdownTooltipText
|
||||
anchors.margins: Style.marginM * scaling
|
||||
anchors.fill: parent
|
||||
text: "Shut down."
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
text: I18n.tr("lock-screen.shut-down")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
@@ -779,7 +934,7 @@ Loader {
|
||||
id: iconReboot
|
||||
anchors.centerIn: parent
|
||||
icon: "reboot"
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
pointSize: Style.fontSizeXXXL * scaling
|
||||
color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||
}
|
||||
|
||||
@@ -787,8 +942,8 @@ Loader {
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: 12 * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
anchors.bottomMargin: Style.marginM * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
@@ -798,8 +953,9 @@ Loader {
|
||||
id: restartTooltipText
|
||||
anchors.margins: Style.marginM * scaling
|
||||
anchors.fill: parent
|
||||
text: "Restart."
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
text: I18n.tr("lock-screen.restart")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
@@ -831,7 +987,7 @@ Loader {
|
||||
id: iconSuspend
|
||||
anchors.centerIn: parent
|
||||
icon: "suspend"
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
pointSize: Style.fontSizeXXXL * scaling
|
||||
color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary
|
||||
}
|
||||
|
||||
@@ -839,8 +995,8 @@ Loader {
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: 12 * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
anchors.bottomMargin: Style.marginM * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
@@ -850,8 +1006,9 @@ Loader {
|
||||
id: suspendTooltipText
|
||||
anchors.margins: Style.marginM * scaling
|
||||
anchors.fill: parent
|
||||
text: "Suspend."
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
text: I18n.tr("lock-screen.suspend")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
@@ -877,8 +1034,8 @@ Loader {
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
timeText.text = Qt.formatDateTime(new Date(), "HH:mm")
|
||||
dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d")
|
||||
timeText.text = formatTime()
|
||||
dateText.text = formatDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,97 +16,113 @@ Variants {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property real scaling: ScalingService.getScreenScale(modelData)
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
// Access the notification model from the service
|
||||
property ListModel notificationModel: NotificationService.notificationModel
|
||||
|
||||
// Track notifications being removed for animation
|
||||
property var removingNotifications: ({})
|
||||
// Access the notification model from the service - UPDATED NAME
|
||||
property ListModel notificationModel: NotificationService.activeList
|
||||
|
||||
// If no notification display activated in settings, then show them all
|
||||
active: Settings.isLoaded && modelData && (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false
|
||||
active: modelData && (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0))
|
||||
|
||||
visible: (NotificationService.notificationModel.count > 0)
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if (root.modelData && screenName === root.modelData.name) {
|
||||
root.scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
screen: modelData
|
||||
|
||||
WlrLayershell.namespace: "noctalia-notifications"
|
||||
WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top
|
||||
|
||||
color: Color.transparent
|
||||
|
||||
// Position based on bar location - always at top
|
||||
anchors.top: true
|
||||
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||
anchors.left: Settings.data.bar.position === "left"
|
||||
readonly property string location: (Settings.data.notifications && Settings.data.notifications.location) ? Settings.data.notifications.location : "top_right"
|
||||
readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top")
|
||||
readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom")
|
||||
readonly property bool isLeft: location.indexOf("_left") >= 0
|
||||
readonly property bool isRight: location.indexOf("_right") >= 0
|
||||
readonly property bool isCentered: (location === "top" || location === "bottom")
|
||||
|
||||
// Anchor selection based on location (window edges)
|
||||
anchors.top: isTop
|
||||
anchors.bottom: isBottom
|
||||
anchors.left: isLeft
|
||||
anchors.right: isRight
|
||||
|
||||
// Margins depending on bar position and chosen location
|
||||
margins.top: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "top":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
return Style.marginM * scaling
|
||||
if (!(anchors.top))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "top") {
|
||||
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraV
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.bottom: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "bottom":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
if (!(anchors.bottom))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "bottom") {
|
||||
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraV
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.left: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "left":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
if (!(anchors.left))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "left") {
|
||||
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraH
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.right: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "right":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
|
||||
case "top":
|
||||
case "bottom":
|
||||
return Style.marginM * scaling
|
||||
default:
|
||||
if (!(anchors.right))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "right") {
|
||||
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraH
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
implicitWidth: 360 * scaling
|
||||
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
|
||||
//WlrLayershell.layer: WlrLayer.Overlay
|
||||
implicitHeight: notificationStack.implicitHeight
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Connect to animation signal from service
|
||||
// Connect to animation signal from service - UPDATED TO USE ID
|
||||
Component.onCompleted: {
|
||||
NotificationService.animateAndRemove.connect(function (notification, index) {
|
||||
// Prefer lookup by identity to avoid index mismatches
|
||||
NotificationService.animateAndRemove.connect(function (notificationId) {
|
||||
// Find the delegate by notification ID
|
||||
var delegate = null
|
||||
if (notificationStack && notificationStack.children && notificationStack.children.length > 0) {
|
||||
for (var i = 0; i < notificationStack.children.length; i++) {
|
||||
var child = notificationStack.children[i]
|
||||
if (child && child.model && child.model.rawNotification === notification) {
|
||||
if (child && child.notificationId === notificationId) {
|
||||
delegate = child
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to index if identity lookup failed
|
||||
if (!delegate && notificationStack && notificationStack.children && notificationStack.children[index]) {
|
||||
delegate = notificationStack.children[index]
|
||||
}
|
||||
|
||||
if (delegate && delegate.animateOut) {
|
||||
delegate.animateOut()
|
||||
} else {
|
||||
// As a last resort, force-remove without animation to avoid stuck popups
|
||||
NotificationService.forceRemoveNotification(notification)
|
||||
// Force removal without animation as fallback
|
||||
NotificationService.dismissActiveNotification(notificationId)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -114,10 +130,12 @@ Variants {
|
||||
// Main notification container
|
||||
ColumnLayout {
|
||||
id: notificationStack
|
||||
// Position based on bar location - always at top
|
||||
anchors.top: parent.top
|
||||
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
|
||||
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
|
||||
// Anchor the stack inside the window based on chosen location
|
||||
anchors.top: parent.isTop ? parent.top : undefined
|
||||
anchors.bottom: parent.isBottom ? parent.bottom : undefined
|
||||
anchors.left: parent.isLeft ? parent.left : undefined
|
||||
anchors.right: parent.isRight ? parent.right : undefined
|
||||
anchors.horizontalCenter: parent.isCentered ? parent.horizontalCenter : undefined
|
||||
spacing: Style.marginS * scaling
|
||||
width: 360 * scaling
|
||||
visible: true
|
||||
@@ -126,6 +144,9 @@ Variants {
|
||||
Repeater {
|
||||
model: notificationModel
|
||||
delegate: Rectangle {
|
||||
// Store the notification ID for reference
|
||||
property string notificationId: model.id
|
||||
|
||||
Layout.preferredWidth: 360 * scaling
|
||||
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
|
||||
Layout.maximumHeight: Layout.preferredHeight
|
||||
@@ -135,6 +156,32 @@ Variants {
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
color: Color.mSurface
|
||||
|
||||
Rectangle {
|
||||
id: progressBar
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2 * scaling
|
||||
color: Color.transparent
|
||||
|
||||
property real availableWidth: parent.width - (2 * parent.radius)
|
||||
|
||||
Rectangle {
|
||||
x: parent.parent.radius + (parent.availableWidth * (1 - model.progress)) / 2
|
||||
width: parent.availableWidth * model.progress
|
||||
height: parent.height
|
||||
color: {
|
||||
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
|
||||
return Color.mError
|
||||
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
|
||||
return Color.mOnSurface
|
||||
else
|
||||
return Color.mPrimary
|
||||
}
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
// Animation properties
|
||||
property real scaleValue: 0.8
|
||||
property real opacityValue: 0.0
|
||||
@@ -174,14 +221,14 @@ Variants {
|
||||
interval: Style.animationSlow
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
NotificationService.forceRemoveNotification(model.rawNotification)
|
||||
// Use the new API method with notification ID
|
||||
NotificationService.dismissActiveNotification(notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this notification is being removed
|
||||
onIsRemovingChanged: {
|
||||
if (isRemoving) {
|
||||
// Remove from model after animation completes
|
||||
removalTimer.start()
|
||||
}
|
||||
}
|
||||
@@ -191,7 +238,6 @@ Variants {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.OutExpo
|
||||
//easing.type: Easing.OutBack looks better but notification get clipped on all sides
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,44 +255,28 @@ Variants {
|
||||
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Header section with app name and timestamp
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NText {
|
||||
text: `${(model.appName || model.desktopEntry) || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6 * scaling
|
||||
Layout.preferredHeight: 6 * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Main content section
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Image
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
imagePath: model.image && model.image !== "" ? model.image : ""
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
visible: (model.image && model.image !== "")
|
||||
ColumnLayout {
|
||||
// For real-time notification always show the original image
|
||||
// as the cached version is most likely still processing.
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: 30 * scaling
|
||||
imagePath: model.originalImage || ""
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
fallbackIcon: "bell"
|
||||
fallbackIconSize: 24 * scaling
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Text content
|
||||
@@ -254,78 +284,118 @@ Variants {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Header section with app name and timestamp
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6 * scaling
|
||||
Layout.preferredHeight: 6 * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: {
|
||||
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
|
||||
return Color.mError
|
||||
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
|
||||
return Color.mOnSurface
|
||||
else
|
||||
return Color.mPrimary
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${model.appName || I18n.tr("system.unknown-app")} · ${Time.formatRelativeTime(model.timestamp)}`
|
||||
color: Color.mSecondary
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.summary || "No summary"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: model.summary || I18n.tr("general.no-summary")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.body || ""
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 5
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification actions
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
visible: model.rawNotification && model.rawNotification.actions && model.rawNotification.actions.length > 0
|
||||
// Notification actions
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
|
||||
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
|
||||
flow: Flow.LeftToRight
|
||||
layoutDirection: Qt.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: parent.notificationActions
|
||||
// Store the notification ID for access in button delegates
|
||||
property string parentNotificationId: notificationId
|
||||
|
||||
delegate: NButton {
|
||||
text: {
|
||||
var actionText = modelData.text || "Open"
|
||||
// If text contains comma, take the part after the comma (the display text)
|
||||
if (actionText.includes(",")) {
|
||||
return actionText.split(",")[1] || actionText
|
||||
// Parse actions from JSON string
|
||||
property var parsedActions: {
|
||||
try {
|
||||
return model.actionsJson ? JSON.parse(model.actionsJson) : []
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
return actionText
|
||||
}
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
hoverColor: Color.mSecondary
|
||||
pressColor: Color.mTertiary
|
||||
outlined: false
|
||||
customHeight: 32 * scaling
|
||||
Layout.preferredHeight: 32 * scaling
|
||||
visible: parsedActions.length > 0
|
||||
|
||||
onClicked: {
|
||||
if (modelData && modelData.invoke) {
|
||||
modelData.invoke()
|
||||
Repeater {
|
||||
model: parent.parsedActions
|
||||
|
||||
delegate: NButton {
|
||||
property var actionData: modelData
|
||||
|
||||
text: {
|
||||
var actionText = actionData.text || "Open"
|
||||
// If text contains comma, take the part after the comma (the display text)
|
||||
if (actionText.includes(",")) {
|
||||
return actionText.split(",")[1] || actionText
|
||||
}
|
||||
return actionText
|
||||
}
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: hovered ? Color.mOnTertiary : Color.mOnPrimary
|
||||
hoverColor: Color.mTertiary
|
||||
outlined: false
|
||||
implicitHeight: 24 * scaling
|
||||
onClicked: {
|
||||
NotificationService.invokeAction(parent.parentNotificationId, actionData.identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to push buttons to the left if needed
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close button positioned absolutely
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.6
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.marginM * scaling
|
||||
|
||||
@@ -13,7 +13,7 @@ NPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: 380
|
||||
preferredHeight: 500
|
||||
preferredHeight: 480
|
||||
panelKeyboardFocus: true
|
||||
|
||||
panelContent: Rectangle {
|
||||
@@ -32,13 +32,13 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "bell"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Notification History"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("notifications.panel.title")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
@@ -46,29 +46,27 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: "Clear history."
|
||||
tooltipText: I18n.tr("tooltips.clear-history")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
NotificationService.clearHistory()
|
||||
// Close panel as there is nothing more to see.
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +79,7 @@ NPanel {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: NotificationService.historyModel.count === 0
|
||||
visible: NotificationService.historyList.count === 0
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
Item {
|
||||
@@ -90,21 +88,21 @@ NPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "bell-off"
|
||||
font.pointSize: 64 * scaling
|
||||
pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "No notifications"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("notifications.panel.no-notifications")
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Your notifications will show up here as they arrive."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
text: I18n.tr("notifications.panel.description")
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
@@ -125,13 +123,15 @@ NPanel {
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
model: NotificationService.historyModel
|
||||
model: NotificationService.historyList
|
||||
spacing: Style.marginM * scaling
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
visible: NotificationService.historyModel.count > 0
|
||||
visible: NotificationService.historyList.count > 0
|
||||
|
||||
delegate: Rectangle {
|
||||
property string notificationId: model.id
|
||||
|
||||
width: notificationList.width
|
||||
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
|
||||
radius: Style.radiusM * scaling
|
||||
@@ -139,82 +139,121 @@ NPanel {
|
||||
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Smooth color transition on hover
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: notificationLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// App icon (same style as popup)
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 28 * scaling
|
||||
Layout.preferredHeight: 28 * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
// Prefer stable themed icons over transient image paths
|
||||
imagePath: (appIcon && appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable") || appIcon) : ((AppIcons.iconForAppId(desktopEntry || appName, "application-x-executable") || (image && image !== "" ? image : AppIcons.iconFromName("application-x-executable", "application-x-executable"))))
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
visible: true
|
||||
ColumnLayout {
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: 20 * scaling
|
||||
imagePath: model.cachedImage || model.originalImage || ""
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
fallbackIcon: "bell"
|
||||
fallbackIconSize: 24 * scaling
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Notification content column
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.maximumWidth: notificationList.width - (Style.marginM * scaling * 4) // Account for margins and delete button
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
// Header row with app name and timestamp
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Urgency indicator
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6 * scaling
|
||||
Layout.preferredHeight: 6 * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: 3 * scaling
|
||||
visible: model.urgency !== 1
|
||||
color: {
|
||||
if (model.urgency === 2)
|
||||
return Color.mError
|
||||
else if (model.urgency === 0)
|
||||
return Color.mOnSurfaceVariant
|
||||
else
|
||||
return Color.transparent
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.appName || "Unknown App"
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mSecondary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Time.formatRelativeTime(model.timestamp)
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mSecondary
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
NText {
|
||||
text: (summary || "No summary").substring(0, 100)
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
text: model.summary || I18n.tr("general.no-summary")
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Font.Medium
|
||||
color: Color.mPrimary
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Body
|
||||
NText {
|
||||
text: (body || "").substring(0, 150)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurface
|
||||
text: model.body || ""
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NotificationService.formatTimestamp(timestamp)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Delete button
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: "Delete notification."
|
||||
tooltipText: I18n.tr("tooltips.delete-notification")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
onClicked: {
|
||||
Logger.log("NotificationHistory", "Removing notification:", summary)
|
||||
NotificationService.historyModel.remove(index)
|
||||
NotificationService.saveHistory()
|
||||
// Remove from history using the service API
|
||||
NotificationService.removeFromHistory(notificationId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: notificationMouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: Style.marginXL * scaling
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
544
Modules/OSD/OSD.qml
Normal file
544
Modules/OSD/OSD.qml
Normal file
@@ -0,0 +1,544 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Unified OSD component
|
||||
// Loader activates only when showing OSD, deactivates when hidden to save resources
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Loader {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
// Access the notification model from the service
|
||||
property ListModel notificationModel: NotificationService.activeList
|
||||
|
||||
// If no notification display activated in settings, then show them all
|
||||
property bool canShowOnThisScreen: modelData && (Settings.data.osd.monitors.includes(modelData.name) || (Settings.data.osd.monitors.length === 0))
|
||||
|
||||
// Loader is only active when actually showing something
|
||||
active: false
|
||||
|
||||
// Current OSD display state
|
||||
property string currentOSDType: "" // "volume", "brightness", or ""
|
||||
|
||||
// Volume properties
|
||||
readonly property real currentVolume: AudioService.volume
|
||||
readonly property bool isMuted: AudioService.muted
|
||||
property bool volumeInitialized: false
|
||||
property bool muteInitialized: false
|
||||
|
||||
// Brightness properties
|
||||
property bool brightnessInitialized: false
|
||||
readonly property real currentBrightness: {
|
||||
if (BrightnessService.monitors.length > 0) {
|
||||
return BrightnessService.monitors[0].brightness || 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get appropriate icon based on current OSD type
|
||||
function getIcon() {
|
||||
if (currentOSDType === "volume") {
|
||||
if (AudioService.muted) {
|
||||
return "volume-mute"
|
||||
}
|
||||
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
|
||||
} else if (currentOSDType === "brightness") {
|
||||
return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get current value (0-1 range)
|
||||
function getCurrentValue() {
|
||||
if (currentOSDType === "volume") {
|
||||
return isMuted ? 0 : currentVolume
|
||||
} else if (currentOSDType === "brightness") {
|
||||
return currentBrightness
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get display percentage
|
||||
function getDisplayPercentage() {
|
||||
if (currentOSDType === "volume") {
|
||||
if (isMuted)
|
||||
return "0%"
|
||||
const pct = Math.round(Math.min(1.0, currentVolume) * 100)
|
||||
return pct + "%"
|
||||
} else if (currentOSDType === "brightness") {
|
||||
const pct = Math.round(Math.min(1.0, currentBrightness) * 100)
|
||||
return pct + "%"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get progress bar color
|
||||
function getProgressColor() {
|
||||
if (currentOSDType === "volume") {
|
||||
if (isMuted)
|
||||
return Color.mError
|
||||
return Color.mPrimary
|
||||
}
|
||||
return Color.mPrimary
|
||||
}
|
||||
|
||||
// Get icon color
|
||||
function getIconColor() {
|
||||
if (currentOSDType === "volume" && isMuted) {
|
||||
return Color.mError
|
||||
}
|
||||
return Color.mOnSurface
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: panel
|
||||
screen: modelData
|
||||
|
||||
readonly property string location: (Settings.data.osd && Settings.data.osd.location) ? Settings.data.osd.location : "top_right"
|
||||
readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top")
|
||||
readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom")
|
||||
readonly property bool isLeft: (location.indexOf("_left") >= 0) || (location === "left")
|
||||
readonly property bool isRight: (location.indexOf("_right") >= 0) || (location === "right")
|
||||
readonly property bool isCentered: (location === "top" || location === "bottom")
|
||||
readonly property bool verticalMode: (location === "left" || location === "right")
|
||||
readonly property int hWidth: Math.round(320 * root.scaling)
|
||||
readonly property int hHeight: Math.round(64 * root.scaling)
|
||||
// Ensure an even width to keep the vertical bar perfectly centered
|
||||
readonly property int barThickness: (function () {
|
||||
const base = Math.max(6, Math.round(6 * root.scaling))
|
||||
return (base % 2 === 0) ? base : base + 1
|
||||
})()
|
||||
|
||||
// Anchor selection based on location (window edges)
|
||||
anchors.top: isTop
|
||||
anchors.bottom: isBottom
|
||||
anchors.left: isLeft
|
||||
anchors.right: isRight
|
||||
|
||||
// Margins depending on bar position and chosen location
|
||||
margins.top: {
|
||||
if (!(anchors.top))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "top") {
|
||||
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraV
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.bottom: {
|
||||
if (!(anchors.bottom))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "bottom") {
|
||||
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraV
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.left: {
|
||||
if (!(anchors.left))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "left") {
|
||||
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraH
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.right: {
|
||||
if (!(anchors.right))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "right") {
|
||||
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraH
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
implicitWidth: verticalMode ? hHeight : hWidth
|
||||
implicitHeight: osdItem.height
|
||||
|
||||
color: Color.transparent
|
||||
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
exclusionMode: PanelWindow.ExclusionMode.Ignore
|
||||
|
||||
Rectangle {
|
||||
id: osdItem
|
||||
|
||||
width: parent.width
|
||||
height: panel.verticalMode ? panel.hWidth : Math.round(64 * root.scaling)
|
||||
radius: Style.radiusL * root.scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: (function () {
|
||||
const bw = Math.max(2, Math.round(Style.borderM * root.scaling))
|
||||
return (bw % 2 === 0) ? bw : bw + 1
|
||||
})()
|
||||
visible: false
|
||||
opacity: 0
|
||||
scale: 0.85
|
||||
|
||||
anchors.horizontalCenter: verticalMode ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: verticalMode ? parent.verticalCenter : undefined
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
id: opacityAnimation
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
id: scaleAnimation
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Settings.data.osd.autoHideMs
|
||||
onTriggered: osdItem.hide()
|
||||
}
|
||||
|
||||
// Timer to handle visibility after animations complete
|
||||
Timer {
|
||||
id: visibilityTimer
|
||||
interval: Style.animationNormal + 50 // Add small buffer
|
||||
onTriggered: {
|
||||
osdItem.visible = false
|
||||
root.currentOSDType = ""
|
||||
// Deactivate the loader when done
|
||||
root.active = false
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: true
|
||||
sourceComponent: verticalMode ? verticalContent : horizontalContent
|
||||
}
|
||||
|
||||
Component {
|
||||
id: horizontalContent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Style.marginM * root.scaling
|
||||
spacing: Style.marginM * root.scaling
|
||||
|
||||
NIcon {
|
||||
icon: root.getIcon()
|
||||
color: root.getIconColor()
|
||||
pointSize: Style.fontSizeXL * root.scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bar with calculated width
|
||||
Rectangle {
|
||||
Layout.preferredWidth: Math.round(220 * root.scaling)
|
||||
height: panel.barThickness
|
||||
radius: Math.round(panel.barThickness / 2)
|
||||
color: Color.mSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * Math.min(1.0, root.getCurrentValue())
|
||||
radius: parent.radius
|
||||
color: root.getProgressColor()
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Percentage text
|
||||
NText {
|
||||
text: root.getDisplayPercentage()
|
||||
color: Color.mOnSurface
|
||||
pointSize: Style.fontSizeS * root.scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Layout.preferredWidth: Math.round(50 * root.scaling)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: verticalContent
|
||||
ColumnLayout {
|
||||
// Ensure inner padding respects the rounded corners; avoid clipping the icon/text
|
||||
property int vMargin: (function () {
|
||||
const styleMargin = Math.round(Style.marginL * root.scaling)
|
||||
const cornerGuard = Math.round(osdItem.radius)
|
||||
return Math.max(styleMargin, cornerGuard)
|
||||
})()
|
||||
property int vMarginTop: Math.max(Math.round(osdItem.radius), Math.round(Style.marginS * root.scaling))
|
||||
property int balanceDelta: Math.round(Style.marginS * root.scaling)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: vMarginTop
|
||||
anchors.bottomMargin: vMargin
|
||||
width: (function () {
|
||||
const w = parent.width - (vMargin * 2)
|
||||
return (w % 2 === 0) ? w : w - 1
|
||||
})()
|
||||
spacing: Math.round(Style.marginS * root.scaling)
|
||||
|
||||
// Percentage text at top
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: percentText.implicitHeight
|
||||
NText {
|
||||
id: percentText
|
||||
text: root.getDisplayPercentage()
|
||||
color: Color.mOnSurface
|
||||
pointSize: Style.fontSizeS * root.scaling
|
||||
family: Settings.data.ui.fontFixed
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: panel.barThickness
|
||||
radius: Math.round(panel.barThickness / 2)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: parent.height * Math.min(1.0, root.getCurrentValue())
|
||||
radius: parent.radius
|
||||
color: root.getProgressColor()
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icon at bottom
|
||||
NIcon {
|
||||
icon: root.getIcon()
|
||||
color: root.getIconColor()
|
||||
pointSize: Style.fontSizeXL * root.scaling
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
||||
Layout.bottomMargin: vMargin + Math.round(Style.marginM * root.scaling) + balanceDelta
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
// Cancel any pending hide operations
|
||||
hideTimer.stop()
|
||||
visibilityTimer.stop()
|
||||
|
||||
// Make visible and animate in
|
||||
osdItem.visible = true
|
||||
// Use Qt.callLater to ensure the visible change is processed before animation
|
||||
Qt.callLater(() => {
|
||||
osdItem.opacity = 1
|
||||
osdItem.scale = 1.0
|
||||
})
|
||||
|
||||
// Start the auto-hide timer
|
||||
hideTimer.start()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
hideTimer.stop()
|
||||
visibilityTimer.stop()
|
||||
|
||||
// Start fade out animation
|
||||
osdItem.opacity = 0
|
||||
osdItem.scale = 0.85 // Less dramatic scale change for smoother effect
|
||||
|
||||
// Delay hiding the element until after animation completes
|
||||
visibilityTimer.start()
|
||||
}
|
||||
|
||||
function hideImmediately() {
|
||||
hideTimer.stop()
|
||||
visibilityTimer.stop()
|
||||
osdItem.opacity = 0
|
||||
osdItem.scale = 0.85
|
||||
osdItem.visible = false
|
||||
root.currentOSDType = ""
|
||||
root.active = false
|
||||
}
|
||||
}
|
||||
|
||||
function showOSD() {
|
||||
osdItem.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Volume change monitoring
|
||||
Connections {
|
||||
target: AudioService
|
||||
|
||||
function onVolumeChanged() {
|
||||
if (volumeInitialized) {
|
||||
showOSD("volume")
|
||||
}
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
if (muteInitialized) {
|
||||
showOSD("volume")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to initialize volume/mute flags after services are ready
|
||||
Timer {
|
||||
id: initTimer
|
||||
interval: 500
|
||||
running: true
|
||||
onTriggered: {
|
||||
volumeInitialized = true
|
||||
muteInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
// Brightness change monitoring
|
||||
Connections {
|
||||
target: BrightnessService
|
||||
|
||||
function onMonitorsChanged() {
|
||||
connectBrightnessMonitors()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
connectBrightnessMonitors()
|
||||
}
|
||||
|
||||
function connectBrightnessMonitors() {
|
||||
for (var i = 0; i < BrightnessService.monitors.length; i++) {
|
||||
let monitor = BrightnessService.monitors[i]
|
||||
// Disconnect first to avoid duplicate connections
|
||||
monitor.brightnessUpdated.disconnect(onBrightnessChanged)
|
||||
monitor.brightnessUpdated.connect(onBrightnessChanged)
|
||||
}
|
||||
}
|
||||
|
||||
function onBrightnessChanged(newBrightness) {
|
||||
if (!brightnessInitialized) {
|
||||
brightnessInitialized = true
|
||||
} else {
|
||||
showOSD("brightness")
|
||||
}
|
||||
}
|
||||
|
||||
function showOSD(type) {
|
||||
// Check if OSD is enabled in settings and can show on this screen
|
||||
if (!Settings.data.osd.enabled || !canShowOnThisScreen) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the current OSD type
|
||||
currentOSDType = type
|
||||
|
||||
// Activate the loader if not already active
|
||||
if (!root.active) {
|
||||
root.active = true
|
||||
}
|
||||
|
||||
// Show the OSD (may need to wait for loader to create the item)
|
||||
if (root.item) {
|
||||
root.item.showOSD()
|
||||
} else {
|
||||
// If item not ready yet, wait for it
|
||||
Qt.callLater(() => {
|
||||
if (root.item) {
|
||||
root.item.showOSD()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function hideOSD() {
|
||||
if (root.item && root.item.osdItem) {
|
||||
root.item.osdItem.hideImmediately()
|
||||
} else if (root.active) {
|
||||
// If loader is active but item isn't ready, just deactivate
|
||||
root.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ NPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: 440
|
||||
preferredHeight: 410
|
||||
preferredHeight: 480
|
||||
panelAnchorHorizontalCenter: true
|
||||
panelAnchorVerticalCenter: true
|
||||
panelKeyboardFocus: true
|
||||
@@ -30,28 +30,33 @@ NPanel {
|
||||
readonly property var powerOptions: [{
|
||||
"action": "lock",
|
||||
"icon": "lock",
|
||||
"title": "Lock",
|
||||
"subtitle": "Lock your session"
|
||||
"title": I18n.tr("session-menu.lock"),
|
||||
"subtitle": I18n.tr("session-menu.lock-subtitle")
|
||||
}, {
|
||||
"action": "lockAndSuspend",
|
||||
"icon": "lock-pause",
|
||||
"title": I18n.tr("session-menu.lock-and-suspend"),
|
||||
"subtitle": I18n.tr("session-menu.lock-and-suspend-subtitle")
|
||||
}, {
|
||||
"action": "suspend",
|
||||
"icon": "suspend",
|
||||
"title": "Suspend",
|
||||
"subtitle": "Put the system to sleep"
|
||||
"title": I18n.tr("session-menu.suspend"),
|
||||
"subtitle": I18n.tr("session-menu.suspend-subtitle")
|
||||
}, {
|
||||
"action": "reboot",
|
||||
"icon": "reboot",
|
||||
"title": "Reboot",
|
||||
"subtitle": "Restart the system"
|
||||
"title": I18n.tr("session-menu.reboot"),
|
||||
"subtitle": I18n.tr("session-menu.reboot-subtitle")
|
||||
}, {
|
||||
"action": "logout",
|
||||
"icon": "logout",
|
||||
"title": "Logout",
|
||||
"subtitle": "End your session"
|
||||
"title": I18n.tr("session-menu.logout"),
|
||||
"subtitle": I18n.tr("session-menu.logout-subtitle")
|
||||
}, {
|
||||
"action": "shutdown",
|
||||
"icon": "shutdown",
|
||||
"title": "Shutdown",
|
||||
"subtitle": "Turn off the system",
|
||||
"title": I18n.tr("session-menu.shutdown"),
|
||||
"subtitle": I18n.tr("session-menu.shutdown-subtitle"),
|
||||
"isShutdown": true
|
||||
}]
|
||||
|
||||
@@ -97,6 +102,9 @@ NPanel {
|
||||
lockScreen.active = true
|
||||
}
|
||||
break
|
||||
case "lockAndSuspend":
|
||||
CompositorService.lockAndSuspend()
|
||||
break
|
||||
case "suspend":
|
||||
CompositorService.suspend()
|
||||
break
|
||||
@@ -263,9 +271,12 @@ NPanel {
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling
|
||||
|
||||
NText {
|
||||
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power Menu"
|
||||
text: timerActive ? I18n.tr("session-menu.action-in-seconds", {
|
||||
"action": pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1),
|
||||
"seconds": Math.ceil(timeRemaining / 1000)
|
||||
}) : I18n.tr("session-menu.title")
|
||||
font.weight: Style.fontWeightBold
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
color: timerActive ? Color.mPrimary : Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -277,7 +288,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: timerActive ? "stop" : "close"
|
||||
tooltipText: timerActive ? "Cancel Timer" : "Close"
|
||||
tooltipText: timerActive ? I18n.tr("tooltips.cancel-timer") : I18n.tr("tooltips.close")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
|
||||
colorFg: timerActive ? Color.mError : Color.mOnSurface
|
||||
@@ -374,7 +385,7 @@ NPanel {
|
||||
return Color.mOnTertiary
|
||||
return Color.mOnSurface
|
||||
}
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
pointSize: Style.fontSizeXXXL * scaling
|
||||
width: Style.baseWidgetSize * 0.6 * scaling
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -398,7 +409,7 @@ NPanel {
|
||||
NText {
|
||||
text: buttonRoot.title
|
||||
font.weight: Style.fontWeightMedium
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
color: {
|
||||
if (buttonRoot.pending)
|
||||
return Color.mPrimary
|
||||
@@ -419,11 +430,11 @@ NPanel {
|
||||
NText {
|
||||
text: {
|
||||
if (buttonRoot.pending) {
|
||||
return "Click again to execute immediately"
|
||||
return I18n.tr("session-menu.click-again")
|
||||
}
|
||||
return buttonRoot.subtitle
|
||||
}
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
pointSize: Style.fontSizeXS * scaling
|
||||
color: {
|
||||
if (buttonRoot.pending)
|
||||
return Color.mPrimary
|
||||
@@ -453,7 +464,7 @@ NPanel {
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: Math.ceil(timeRemaining / 1000)
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
@@ -20,6 +20,7 @@ NBox {
|
||||
signal removeWidget(string section, int index)
|
||||
signal reorderWidget(string section, int fromIndex, int toIndex)
|
||||
signal updateWidgetSettings(string section, int index, var settings)
|
||||
signal moveWidget(string fromSection, int index, string toSection)
|
||||
signal dragPotentialStarted
|
||||
signal dragPotentialEnded
|
||||
|
||||
@@ -69,7 +70,7 @@ NBox {
|
||||
|
||||
NText {
|
||||
text: sectionName + " Section"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
@@ -78,16 +79,29 @@ NBox {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NComboBox {
|
||||
NSearchableComboBox {
|
||||
id: comboBox
|
||||
model: availableWidgets
|
||||
label: ""
|
||||
description: ""
|
||||
placeholder: "Select a widget to add..."
|
||||
placeholder: I18n.tr("bar.widget-settings.section-editor.placeholder")
|
||||
searchPlaceholder: I18n.tr("bar.widget-settings.section-editor.search-placeholder")
|
||||
onSelected: key => comboBox.currentKey = key
|
||||
popupHeight: 340 * scaling
|
||||
minimumWidth: 200 * scaling
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Re-filter when the model count changes (when widgets are loaded)
|
||||
Connections {
|
||||
target: availableWidgets
|
||||
function onCountChanged() {
|
||||
// Trigger a re-filter by clearing and re-setting the search text
|
||||
var currentSearch = comboBox.searchText
|
||||
comboBox.searchText = ""
|
||||
comboBox.searchText = currentSearch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
@@ -98,7 +112,7 @@ NBox {
|
||||
colorBgHover: Color.mSecondary
|
||||
colorFgHover: Color.mOnSecondary
|
||||
enabled: comboBox.currentKey !== ""
|
||||
tooltipText: "Add widget to section"
|
||||
tooltipText: I18n.tr("tooltips.add-widget")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
onClicked: {
|
||||
@@ -125,6 +139,7 @@ NBox {
|
||||
|
||||
Repeater {
|
||||
model: widgetModel
|
||||
|
||||
delegate: Rectangle {
|
||||
id: widgetItem
|
||||
required property int index
|
||||
@@ -158,6 +173,51 @@ NBox {
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu for moving widget to other sections
|
||||
NContextMenu {
|
||||
id: contextMenu
|
||||
parent: Overlay.overlay
|
||||
width: 240 * scaling
|
||||
model: [{
|
||||
"label": I18n.tr("tooltips.move-to-left-section"),
|
||||
"action": "left",
|
||||
"icon": "arrow-bar-to-left",
|
||||
"visible": root.sectionId !== "left"
|
||||
}, {
|
||||
"label": I18n.tr("tooltips.move-to-center-section"),
|
||||
"action": "center",
|
||||
"icon": "layout-columns",
|
||||
"visible": root.sectionId !== "center"
|
||||
}, {
|
||||
"label": I18n.tr("tooltips.move-to-right-section"),
|
||||
"action": "right",
|
||||
"icon": "arrow-bar-to-right",
|
||||
"visible": root.sectionId !== "right"
|
||||
}]
|
||||
|
||||
onTriggered: action => root.moveWidget(root.sectionId, index, action)
|
||||
}
|
||||
|
||||
// Update the MouseArea to use the new context menu
|
||||
MouseArea {
|
||||
id: contextMouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
z: -1 // Below the buttons but above background
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
// Check if click is not on the buttons area
|
||||
const localX = mouse.x
|
||||
const buttonsStartX = parent.width - (parent.buttonsCount * parent.buttonsWidth)
|
||||
|
||||
if (localX < buttonsStartX) {
|
||||
// Use the helper function to open at mouse position
|
||||
contextMenu.openAtItem(widgetItem, mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
id: widgetContent
|
||||
anchors.centerIn: parent
|
||||
@@ -165,7 +225,7 @@ NBox {
|
||||
|
||||
NText {
|
||||
text: modelData.id
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: root.getWidgetColor(modelData)[1]
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
@@ -180,6 +240,7 @@ NBox {
|
||||
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
|
||||
sourceComponent: NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("tooltips.widget-settings")
|
||||
baseSize: miniButtonSize
|
||||
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
|
||||
colorBg: Color.mOnSurface
|
||||
@@ -220,6 +281,7 @@ NBox {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("tooltips.remove-widget")
|
||||
baseSize: miniButtonSize
|
||||
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
|
||||
colorBg: Color.mOnSurface
|
||||
@@ -250,10 +312,10 @@ NBox {
|
||||
z: 2000
|
||||
clip: false // Ensure ghost isn't clipped
|
||||
|
||||
Text {
|
||||
NText {
|
||||
id: ghostText
|
||||
anchors.centerIn: parent
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
import "./WidgetSettings" as WidgetSettings
|
||||
|
||||
// Widget Settings Dialog Component
|
||||
Popup {
|
||||
id: settingsPopup
|
||||
// Don't replace by root!
|
||||
id: widgetSettings
|
||||
|
||||
property int widgetIndex: -1
|
||||
property var widgetData: null
|
||||
@@ -19,56 +19,37 @@ Popup {
|
||||
x: (parent.width - width) * 0.5
|
||||
y: (parent.height - height) * 0.5
|
||||
|
||||
width: 500 * scaling
|
||||
width: Math.max(content.implicitWidth + padding * 2, 500 * scaling)
|
||||
height: content.implicitHeight + padding * 2
|
||||
padding: Style.marginXL * scaling
|
||||
modal: true
|
||||
|
||||
background: Rectangle {
|
||||
id: bgRect
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM * scaling
|
||||
}
|
||||
|
||||
// Load settings when popup opens with data
|
||||
onOpened: {
|
||||
// Mark this popup has opened in the PanelService
|
||||
PanelService.willOpenPopup(widgetSettings)
|
||||
|
||||
// Load settings when popup opens with data
|
||||
if (widgetData && widgetId) {
|
||||
loadWidgetSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function loadWidgetSettings() {
|
||||
const widgetSettingsMap = {
|
||||
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
|
||||
"Battery": "WidgetSettings/BatterySettings.qml",
|
||||
"Brightness": "WidgetSettings/BrightnessSettings.qml",
|
||||
"Clock": "WidgetSettings/ClockSettings.qml",
|
||||
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
|
||||
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
|
||||
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
|
||||
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
|
||||
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
|
||||
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
|
||||
"SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml",
|
||||
"Spacer": "WidgetSettings/SpacerSettings.qml",
|
||||
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
|
||||
"Volume": "WidgetSettings/VolumeSettings.qml"
|
||||
}
|
||||
|
||||
const source = widgetSettingsMap[widgetId]
|
||||
if (source) {
|
||||
// Use setSource to pass properties at creation time
|
||||
settingsLoader.setSource(source, {
|
||||
"widgetData": widgetData,
|
||||
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
})
|
||||
}
|
||||
onClosed: {
|
||||
PanelService.willClosePopup(widgetSettings)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
background: Rectangle {
|
||||
id: bgRect
|
||||
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: content
|
||||
|
||||
width: parent.width
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
@@ -77,8 +58,10 @@ Popup {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: `${settingsPopup.widgetId} Settings`
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
text: I18n.tr("system.widget-settings-title", {
|
||||
"widget": widgetSettings.widgetId
|
||||
})
|
||||
pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
@@ -86,7 +69,8 @@ Popup {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: settingsPopup.close()
|
||||
tooltipText: "Close"
|
||||
onClicked: widgetSettings.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,22 +99,51 @@ Popup {
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Cancel"
|
||||
text: I18n.tr("bar.widget-settings.dialog.cancel")
|
||||
outlined: true
|
||||
onClicked: settingsPopup.close()
|
||||
onClicked: widgetSettings.close()
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Apply"
|
||||
text: I18n.tr("bar.widget-settings.dialog.apply")
|
||||
icon: "check"
|
||||
onClicked: {
|
||||
if (settingsLoader.item && settingsLoader.item.saveSettings) {
|
||||
var newSettings = settingsLoader.item.saveSettings()
|
||||
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
|
||||
settingsPopup.close()
|
||||
root.updateWidgetSettings(sectionId, widgetSettings.widgetIndex, newSettings)
|
||||
widgetSettings.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadWidgetSettings() {
|
||||
const widgetSettingsMap = {
|
||||
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
|
||||
"Battery": "WidgetSettings/BatterySettings.qml",
|
||||
"Brightness": "WidgetSettings/BrightnessSettings.qml",
|
||||
"Clock": "WidgetSettings/ClockSettings.qml",
|
||||
"ControlCenter": "WidgetSettings/ControlCenterSettings.qml",
|
||||
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
|
||||
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
|
||||
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
|
||||
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
|
||||
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
|
||||
"Spacer": "WidgetSettings/SpacerSettings.qml",
|
||||
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
|
||||
"Volume": "WidgetSettings/VolumeSettings.qml",
|
||||
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
|
||||
"Taskbar": "WidgetSettings/TaskbarSettings.qml"
|
||||
}
|
||||
|
||||
const source = widgetSettingsMap[widgetId]
|
||||
if (source) {
|
||||
// Use setSource to pass properties at creation time
|
||||
settingsLoader.setSource(source, {
|
||||
"widgetData": widgetData,
|
||||
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml
Normal file
62
Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml
Normal file
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
|
||||
property bool valueAutoHide: widgetData.autoHide !== undefined ? widgetData.autoHide : widgetMetadata.autoHide
|
||||
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.autoHide = valueAutoHide
|
||||
settings.showIcon = valueShowIcon
|
||||
settings.scrollingMode = valueScrollingMode
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.active-window.auto-hide.label")
|
||||
description: I18n.tr("bar.widget-settings.active-window.auto-hide.description")
|
||||
checked: root.valueAutoHide
|
||||
onToggled: checked => root.valueAutoHide = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.active-window.show-app-icon.label")
|
||||
description: I18n.tr("bar.widget-settings.active-window.show-app-icon.description")
|
||||
checked: root.valueShowIcon
|
||||
onToggled: checked => root.valueShowIcon = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("bar.widget-settings.active-window.scrolling-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.active-window.scrolling-mode.description")
|
||||
model: [{
|
||||
"key": "always",
|
||||
"name": I18n.tr("options.scrolling-modes.always")
|
||||
}, {
|
||||
"key": "hover",
|
||||
"name": I18n.tr("options.scrolling-modes.hover")
|
||||
}, {
|
||||
"key": "never",
|
||||
"name": I18n.tr("options.scrolling-modes.never")
|
||||
}]
|
||||
currentKey: valueScrollingMode
|
||||
onSelected: key => valueScrollingMode = key
|
||||
minimumWidth: 200 * scaling
|
||||
}
|
||||
}
|
||||
@@ -25,30 +25,26 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.battery.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.battery.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("options.display-mode.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: root.valueDisplayMode
|
||||
onSelected: key => root.valueDisplayMode = key
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: "Low battery warning threshold"
|
||||
description: "Show a warning when battery falls below this percentage."
|
||||
label: I18n.tr("bar.widget-settings.battery.low-battery-threshold.label")
|
||||
description: I18n.tr("bar.widget-settings.battery.low-battery-threshold.description")
|
||||
value: valueWarningThreshold
|
||||
suffix: "%"
|
||||
minimum: 5
|
||||
@@ -23,23 +23,19 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.brightness.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.brightness.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("options.display-mode.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
270
Modules/Settings/Bar/WidgetSettings/ClockSettings.qml
Normal file
270
Modules/Settings/Bar/WidgetSettings/ClockSettings.qml
Normal file
@@ -0,0 +1,270 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
width: 700 * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueUsePrimaryColor: widgetData.usePrimaryColor !== undefined ? widgetData.usePrimaryColor : widgetMetadata.usePrimaryColor
|
||||
property bool valueUseCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont
|
||||
property string valueCustomFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.customFont
|
||||
property string valueFormatHorizontal: widgetData.formatHorizontal !== undefined ? widgetData.formatHorizontal : widgetMetadata.formatHorizontal
|
||||
property string valueFormatVertical: widgetData.formatVertical !== undefined ? widgetData.formatVertical : widgetMetadata.formatVertical
|
||||
|
||||
// Track the currently focused input field
|
||||
property var focusedInput: null
|
||||
property int focusedLineIndex: -1
|
||||
|
||||
readonly property var now: Time.date
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.usePrimaryColor = valueUsePrimaryColor
|
||||
settings.useCustomFont = valueUseCustomFont
|
||||
settings.customFont = valueCustomFont
|
||||
settings.formatHorizontal = valueFormatHorizontal.trim()
|
||||
settings.formatVertical = valueFormatVertical.trim()
|
||||
return settings
|
||||
}
|
||||
|
||||
// Function to insert token at cursor position in the focused input
|
||||
function insertToken(token) {
|
||||
if (!focusedInput || !focusedInput.inputItem) {
|
||||
// If no input is focused, default to horiz
|
||||
if (inputHoriz.inputItem) {
|
||||
inputHoriz.inputItem.focus = true
|
||||
focusedInput = inputHoriz
|
||||
}
|
||||
}
|
||||
|
||||
if (focusedInput && focusedInput.inputItem) {
|
||||
var input = focusedInput.inputItem
|
||||
var cursorPos = input.cursorPosition
|
||||
var currentText = input.text
|
||||
|
||||
// Insert token at cursor position
|
||||
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos)
|
||||
input.text = newText + " "
|
||||
|
||||
// Move cursor after the inserted token
|
||||
input.cursorPosition = cursorPos + token.length + 1
|
||||
|
||||
// Ensure the input keeps focus
|
||||
input.focus = true
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.clock.use-primary-color.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.use-primary-color.description")
|
||||
checked: valueUsePrimaryColor
|
||||
onToggled: checked => valueUsePrimaryColor = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.clock.use-custom-font.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.use-custom-font.description")
|
||||
checked: valueUseCustomFont
|
||||
onToggled: checked => valueUseCustomFont = checked
|
||||
}
|
||||
|
||||
NSearchableComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: valueUseCustomFont
|
||||
label: I18n.tr("bar.widget-settings.clock.custom-font.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.custom-font.description")
|
||||
model: FontService.availableFonts
|
||||
currentKey: valueCustomFont
|
||||
placeholder: I18n.tr("bar.widget-settings.clock.custom-font.placeholder")
|
||||
searchPlaceholder: I18n.tr("bar.widget-settings.clock.custom-font.search-placeholder")
|
||||
popupHeight: 420 * scaling
|
||||
minimumWidth: 300 * scaling
|
||||
onSelected: function (key) {
|
||||
valueCustomFont = key
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("bar.widget-settings.clock.clock-display.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.clock-display.description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: main
|
||||
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1 // Equal sizing hint
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
|
||||
NTextInput {
|
||||
id: inputHoriz
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.clock.horizontal-bar.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.horizontal-bar.description")
|
||||
placeholderText: "HH:mm ddd, MMM dd"
|
||||
text: valueFormatHorizontal
|
||||
onTextChanged: valueFormatHorizontal = text
|
||||
Component.onCompleted: {
|
||||
if (inputItem) {
|
||||
inputItem.onActiveFocusChanged.connect(function () {
|
||||
if (inputItem.activeFocus) {
|
||||
root.focusedInput = inputHoriz
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: inputVert
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.clock.vertical-bar.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.vertical-bar.description")
|
||||
// Tokens are Qt format tokens and must not be localized
|
||||
placeholderText: "HH mm dd MM"
|
||||
text: valueFormatVertical
|
||||
onTextChanged: valueFormatVertical = text
|
||||
Component.onCompleted: {
|
||||
if (inputItem) {
|
||||
inputItem.onActiveFocusChanged.connect(function () {
|
||||
if (inputItem.activeFocus) {
|
||||
root.focusedInput = inputVert
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Preview
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
Layout.fillWidth: false
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.widget-settings.clock.preview")
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 320 * scaling
|
||||
Layout.preferredHeight: 160 * scaling // Fixed height instead of fillHeight
|
||||
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM * scaling
|
||||
border.color: Color.mSecondary
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
anchors.centerIn: parent
|
||||
|
||||
ColumnLayout {
|
||||
spacing: -2 * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// Horizontal
|
||||
Repeater {
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
model: Qt.locale().toString(now, valueFormatHorizontal.trim()).split("\\n")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : (valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault)
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Vertical
|
||||
ColumnLayout {
|
||||
spacing: -2 * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Repeater {
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
model: Qt.locale().toString(now, valueFormatVertical.trim()).split(" ")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : (valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault)
|
||||
pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
}
|
||||
|
||||
NDateTimeTokens {
|
||||
Layout.fillWidth: true
|
||||
height: 200 * scaling
|
||||
|
||||
// Connect to token clicked signal if NDateTimeTokens provides it
|
||||
onTokenClicked: token => root.insertToken(token)
|
||||
}
|
||||
}
|
||||
103
Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml
Normal file
103
Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml
Normal file
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
|
||||
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : ""
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.icon = valueIcon
|
||||
settings.useDistroLogo = valueUseDistroLogo
|
||||
settings.customIconPath = valueCustomIconPath
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.widget-settings.control-center.use-distro-logo.label")
|
||||
description: I18n.tr("bar.widget-settings.control-center.use-distro-logo.description")
|
||||
checked: valueUseDistroLogo
|
||||
onToggled: {
|
||||
valueUseDistroLogo = checked
|
||||
if (checked) {
|
||||
valueCustomIconPath = ""
|
||||
valueIcon = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.widget-settings.control-center.icon.label")
|
||||
description: I18n.tr("bar.widget-settings.control-center.icon.description")
|
||||
}
|
||||
|
||||
NImageCircled {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
imagePath: valueCustomIconPath
|
||||
visible: valueCustomIconPath !== ""
|
||||
width: Style.fontSizeXL * 2 * scaling
|
||||
height: Style.fontSizeXL * 2 * scaling
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: valueIcon
|
||||
pointSize: Style.fontSizeXXL * 1.5 * scaling
|
||||
visible: valueIcon !== "" && valueCustomIconPath === ""
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
NButton {
|
||||
enabled: !valueUseDistroLogo
|
||||
text: I18n.tr("bar.widget-settings.control-center.browse-library")
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
|
||||
NButton {
|
||||
enabled: !valueUseDistroLogo
|
||||
text: I18n.tr("bar.widget-settings.control-center.browse-file")
|
||||
onClicked: imagePicker.openFilePicker()
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPicker
|
||||
initialIcon: valueIcon
|
||||
onIconSelected: iconName => {
|
||||
valueIcon = iconName
|
||||
valueCustomIconPath = ""
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: imagePicker
|
||||
title: I18n.tr("bar.widget-settings.control-center.select-custom-icon")
|
||||
selectionMode: "files"
|
||||
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
|
||||
initialPath: Quickshell.env("HOME")
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
valueCustomIconPath = paths[0] // Use first selected file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
Modules/Settings/Bar/WidgetSettings/CustomButtonSettings.qml
Normal file
110
Modules/Settings/Bar/WidgetSettings/CustomButtonSettings.qml
Normal file
@@ -0,0 +1,110 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.icon = valueIcon
|
||||
settings.leftClickExec = leftClickExecInput.text
|
||||
settings.rightClickExec = rightClickExecInput.text
|
||||
settings.middleClickExec = middleClickExecInput.text
|
||||
settings.textCommand = textCommandInput.text
|
||||
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10)
|
||||
return settings
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.widget-settings.custom-button.icon.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.icon.description")
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: valueIcon
|
||||
pointSize: Style.fontSizeXL * scaling
|
||||
visible: valueIcon !== ""
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("bar.widget-settings.custom-button.browse")
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPicker
|
||||
initialIcon: valueIcon
|
||||
onIconSelected: function (iconName) {
|
||||
valueIcon = iconName
|
||||
}
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: leftClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.left-click.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.left-click.description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: rightClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.right-click.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.right-click.description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: middleClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.middle-click.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.middle-click.description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("bar.widget-settings.custom-button.dynamic-text")
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textCommandInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.display-command-output.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.display-command-output.description")
|
||||
placeholderText: I18n.tr("placeholders.command-example")
|
||||
text: widgetData?.textCommand || widgetMetadata.textCommand
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textIntervalInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.refresh-interval.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.refresh-interval.description")
|
||||
placeholderText: String(widgetMetadata.textIntervalMs || 3000)
|
||||
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
|
||||
}
|
||||
}
|
||||
@@ -23,23 +23,19 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.keyboard-layout.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.keyboard-layout.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "forceOpen"
|
||||
name: "Force Open"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "forceOpen",
|
||||
"name": I18n.tr("options.display-mode.force-open")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
91
Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml
Normal file
91
Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml
Normal file
@@ -0,0 +1,91 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueAutoHide: widgetData.autoHide !== undefined ? widgetData.autoHide : widgetMetadata.autoHide
|
||||
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
|
||||
property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType
|
||||
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.autoHide = valueAutoHide
|
||||
settings.showAlbumArt = valueShowAlbumArt
|
||||
settings.showVisualizer = valueShowVisualizer
|
||||
settings.visualizerType = valueVisualizerType
|
||||
settings.scrollingMode = valueScrollingMode
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.media-mini.auto-hide.label")
|
||||
description: I18n.tr("bar.widget-settings.media-mini.auto-hide.description")
|
||||
checked: root.valueAutoHide
|
||||
onToggled: checked => root.valueAutoHide = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.widget-settings.media-mini.show-album-art.label")
|
||||
description: I18n.tr("bar.widget-settings.media-mini.show-album-art.description")
|
||||
checked: valueShowAlbumArt
|
||||
onToggled: checked => valueShowAlbumArt = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.widget-settings.media-mini.show-visualizer.label")
|
||||
description: I18n.tr("bar.widget-settings.media-mini.show-visualizer.description")
|
||||
checked: valueShowVisualizer
|
||||
onToggled: checked => valueShowVisualizer = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
visible: valueShowVisualizer
|
||||
label: I18n.tr("bar.widget-settings.media-mini.visualizer-type.label")
|
||||
description: I18n.tr("bar.widget-settings.media-mini.visualizer-type.description")
|
||||
model: [{
|
||||
"key": "linear",
|
||||
"name": I18n.tr("options.visualizer-types.linear")
|
||||
}, {
|
||||
"key": "mirrored",
|
||||
"name": I18n.tr("options.visualizer-types.mirrored")
|
||||
}, {
|
||||
"key": "wave",
|
||||
"name": I18n.tr("options.visualizer-types.wave")
|
||||
}]
|
||||
currentKey: valueVisualizerType
|
||||
onSelected: key => valueVisualizerType = key
|
||||
minimumWidth: 200 * scaling
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("bar.widget-settings.media-mini.scrolling-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.media-mini.scrolling-mode.description")
|
||||
model: [{
|
||||
"key": "always",
|
||||
"name": I18n.tr("options.scrolling-modes.always")
|
||||
}, {
|
||||
"key": "hover",
|
||||
"name": I18n.tr("options.scrolling-modes.hover")
|
||||
}, {
|
||||
"key": "never",
|
||||
"name": I18n.tr("options.scrolling-modes.never")
|
||||
}]
|
||||
currentKey: valueScrollingMode
|
||||
onSelected: key => valueScrollingMode = key
|
||||
minimumWidth: 200 * scaling
|
||||
}
|
||||
}
|
||||
@@ -23,23 +23,19 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.microphone.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.microphone.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("options.display-mode.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
@@ -25,13 +25,15 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Show unread badge"
|
||||
label: I18n.tr("bar.widget-settings.notification-history.show-unread-badge.label")
|
||||
description: I18n.tr("bar.widget-settings.notification-history.show-unread-badge.description")
|
||||
checked: valueShowUnreadBadge
|
||||
onToggled: checked => valueShowUnreadBadge = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Hide badge when zero"
|
||||
label: I18n.tr("bar.widget-settings.notification-history.hide-badge-when-zero.label")
|
||||
description: I18n.tr("bar.widget-settings.notification-history.hide-badge-when-zero.description")
|
||||
checked: valueHideWhenZero
|
||||
onToggled: checked => valueHideWhenZero = checked
|
||||
}
|
||||
@@ -22,9 +22,9 @@ ColumnLayout {
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: "Width"
|
||||
description: "Spacing width in pixels"
|
||||
label: I18n.tr("bar.widget-settings.spacer.width.label")
|
||||
description: I18n.tr("bar.widget-settings.spacer.width.description")
|
||||
text: widgetData.width || widgetMetadata.width
|
||||
placeholderText: "Enter width in pixels"
|
||||
placeholderText: I18n.tr("placeholders.enter-width-pixels")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user