mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-25 14:02:53 -05:00
Compare commits
406 Commits
v0.0.5
...
surface-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e103d7ba7 | ||
|
|
7ccd2d9418 | ||
|
|
974dd70a06 | ||
|
|
1690e4f63b | ||
|
|
ec75ef468b | ||
|
|
a57c1e6451 | ||
|
|
bdc79a13a9 | ||
|
|
b893694977 | ||
|
|
9be7d44765 | ||
|
|
3f8d8ca379 | ||
|
|
e50c3cceeb | ||
|
|
14e648911d | ||
|
|
3c42a618d4 | ||
|
|
bed2259944 | ||
|
|
7516d44de9 | ||
|
|
e9a61a4f81 | ||
|
|
7a7d7d053a | ||
|
|
2067e44baf | ||
|
|
8e010478c7 | ||
|
|
ec4f0ff2ed | ||
|
|
c04177e45d | ||
|
|
b9b1737639 | ||
|
|
4468e4c6af | ||
|
|
cf5dec733d | ||
|
|
d9b9da4b3d | ||
|
|
d62ef89bc3 | ||
|
|
1b681e68b9 | ||
|
|
d15ee0c29b | ||
|
|
62b7b30754 | ||
|
|
aa52b586d6 | ||
|
|
ca11735c1d | ||
|
|
78683032aa | ||
|
|
bd5064e567 | ||
|
|
3862f0ced5 | ||
|
|
878c1d9385 | ||
|
|
0a5f635a2d | ||
|
|
6a963ed618 | ||
|
|
35f059b1dc | ||
|
|
b7838e497d | ||
|
|
3edfff388c | ||
|
|
0bfaf2a8f8 | ||
|
|
50f6dfa709 | ||
|
|
897f48d151 | ||
|
|
c43f58e1bf | ||
|
|
de532fdeda | ||
|
|
c21896b0df | ||
|
|
1669be4dd9 | ||
|
|
81550229e8 | ||
|
|
f38ffa1de5 | ||
|
|
66b493fb2b | ||
|
|
ceae254160 | ||
|
|
a3b3e49313 | ||
|
|
8fb17f7fd5 | ||
|
|
c464bb3e47 | ||
|
|
bd72cf3adf | ||
|
|
a734d9b497 | ||
|
|
b18814e086 | ||
|
|
74417a18ea | ||
|
|
227081c5c9 | ||
|
|
c3b3edcae8 | ||
|
|
c8f87085a0 | ||
|
|
118375f004 | ||
|
|
babcc66765 | ||
|
|
64c8e79bf2 | ||
|
|
12e8e72bb2 | ||
|
|
72b79c0f51 | ||
|
|
50e440f212 | ||
|
|
7c1c64fefc | ||
|
|
2e4770f4be | ||
|
|
0b2bf0b83d | ||
|
|
1fbb614aa8 | ||
|
|
55a29d8504 | ||
|
|
ffc13f71b7 | ||
|
|
d4816bd174 | ||
|
|
3cac6e5eea | ||
|
|
50bc3db048 | ||
|
|
2b99e61c65 | ||
|
|
306a0c0e2d | ||
|
|
0ba4ee74b5 | ||
|
|
2b5a5115b7 | ||
|
|
8a7e386adb | ||
|
|
6e6412fffc | ||
|
|
bc540056fc | ||
|
|
0055ddbc8d | ||
|
|
3376dc893d | ||
|
|
4a673fe1b5 | ||
|
|
6639f6ead0 | ||
|
|
2ce32e8bb5 | ||
|
|
4d13a3d909 | ||
|
|
691b6da7a7 | ||
|
|
e6265c2f71 | ||
|
|
de9bb43c26 | ||
|
|
1ae9802866 | ||
|
|
0e887408c5 | ||
|
|
88b7657e34 | ||
|
|
5fcffba73e | ||
|
|
542536455b | ||
|
|
97f0acadb6 | ||
|
|
32f3e579e7 | ||
|
|
bc446dabaf | ||
|
|
d8e2a73e0b | ||
|
|
ae6fd42163 | ||
|
|
cc7363eee1 | ||
|
|
38c4b33d15 | ||
|
|
6c81aa0908 | ||
|
|
102cc3c0b6 | ||
|
|
02300024cf | ||
|
|
344761df00 | ||
|
|
c0edaea716 | ||
|
|
75e660f78e | ||
|
|
be4d9cadba | ||
|
|
fdaac6ac8b | ||
|
|
5c540f826e | ||
|
|
55b3bffccd | ||
|
|
0cb2a96b3e | ||
|
|
9f13ac6963 | ||
|
|
19e148f65c | ||
|
|
7d9334916c | ||
|
|
50dedb55ac | ||
|
|
38f61ceae2 | ||
|
|
fff4b8c817 | ||
|
|
0fe76b7575 | ||
|
|
3c4719d430 | ||
|
|
c04e2ae0ff | ||
|
|
fec921df10 | ||
|
|
5186afce06 | ||
|
|
77ea6a9372 | ||
|
|
afb0435b5c | ||
|
|
903ef0cc72 | ||
|
|
e4f86abda9 | ||
|
|
8ee43de145 | ||
|
|
83b29c5ed9 | ||
|
|
cdf4ca3b3b | ||
|
|
a0192e709e | ||
|
|
59c7246989 | ||
|
|
76ee483b27 | ||
|
|
ba6c7ae28c | ||
|
|
07c1677145 | ||
|
|
3db1a2ccf2 | ||
|
|
6d84553da9 | ||
|
|
77bc129dfc | ||
|
|
2c451b9779 | ||
|
|
3c6bb58088 | ||
|
|
0372c44d98 | ||
|
|
8fc40ab12d | ||
|
|
9e1272029e | ||
|
|
27344a47e2 | ||
|
|
c42681b9b9 | ||
|
|
939f47286e | ||
|
|
c8cb5dc146 | ||
|
|
c3759dc542 | ||
|
|
17b49ad1f9 | ||
|
|
63c54b5611 | ||
|
|
988d5c2fc3 | ||
|
|
7a671b586c | ||
|
|
b36da8eba2 | ||
|
|
5b49e36da4 | ||
|
|
2395274714 | ||
|
|
d0cbe689f8 | ||
|
|
4ae157af35 | ||
|
|
fb94dd0c4a | ||
|
|
fb01d1af4b | ||
|
|
9438868706 | ||
|
|
a77f6eb385 | ||
|
|
44ebd6a2ee | ||
|
|
e5083fb3a4 | ||
|
|
5a9c80540e | ||
|
|
5bb489f3c4 | ||
|
|
35ea621a17 | ||
|
|
a4d1b6ad76 | ||
|
|
a67a6c7c1c | ||
|
|
1c7d8a55f3 | ||
|
|
c9d38ec6f3 | ||
|
|
d961c7569b | ||
|
|
70a971ade2 | ||
|
|
5d4ba8b38a | ||
|
|
537b4b3604 | ||
|
|
7105b09dc0 | ||
|
|
e0118987ad | ||
|
|
fca3c1ef96 | ||
|
|
c867090f12 | ||
|
|
ee06338640 | ||
|
|
751aee40a5 | ||
|
|
252cc8f42a | ||
|
|
dc28015313 | ||
|
|
47b6b365a1 | ||
|
|
e2945a6a2a | ||
|
|
3aa8ea55e7 | ||
|
|
2179468caf | ||
|
|
597f748e56 | ||
|
|
ac1d01a190 | ||
|
|
fd623035c7 | ||
|
|
cbcb9a7f89 | ||
|
|
9c2e5561d7 | ||
|
|
9292e15419 | ||
|
|
c78cb3c767 | ||
|
|
3a1a553a7d | ||
|
|
cbf3d0f330 | ||
|
|
9d416ddbd6 | ||
|
|
079faa0d40 | ||
|
|
4c18e6b01d | ||
|
|
fcd818b098 | ||
|
|
cd1c992abd | ||
|
|
cebcb4d5d9 | ||
|
|
68243107ca | ||
|
|
af952f6397 | ||
|
|
7d88f37f90 | ||
|
|
8abc94fd07 | ||
|
|
c23088c98a | ||
|
|
072cbf284e | ||
|
|
f7dfd31256 | ||
|
|
2ef41555a9 | ||
|
|
c9d7641d86 | ||
|
|
5f97fd4f7c | ||
|
|
9ba3bfb4db | ||
|
|
353ee355a3 | ||
|
|
79c21e67dc | ||
|
|
4963658bc2 | ||
|
|
613faea714 | ||
|
|
2d764d8cac | ||
|
|
bd29abc7e3 | ||
|
|
3d3b2726c9 | ||
|
|
0b76171151 | ||
|
|
8d674a4fdc | ||
|
|
68157ca636 | ||
|
|
18484bb9cc | ||
|
|
e02b2580c9 | ||
|
|
0d6dbf5f99 | ||
|
|
ba1125bc00 | ||
|
|
af5094b479 | ||
|
|
4742636eb3 | ||
|
|
c708f57e8f | ||
|
|
54b63a1f5f | ||
|
|
3c3cffd7b6 | ||
|
|
e64124cce3 | ||
|
|
eecbd8c733 | ||
|
|
9e0693ca8c | ||
|
|
998d390161 | ||
|
|
67a16da8c7 | ||
|
|
21867c842f | ||
|
|
f7c45acc26 | ||
|
|
178ccd3634 | ||
|
|
b4e607e2b4 | ||
|
|
3856ce14cd | ||
|
|
d4db8a01fe | ||
|
|
886c6877d5 | ||
|
|
5146dcb3f7 | ||
|
|
73b832eddb | ||
|
|
f5871ab27e | ||
|
|
ae6a1b77c2 | ||
|
|
93da303967 | ||
|
|
2a8087cd27 | ||
|
|
531d6334fb | ||
|
|
21089aa66e | ||
|
|
be42280aca | ||
|
|
123058f770 | ||
|
|
0872499f36 | ||
|
|
cfbeb6062b | ||
|
|
213543acb3 | ||
|
|
5bffb1ba10 | ||
|
|
96db0581d3 | ||
|
|
c4438b3339 | ||
|
|
154b90103e | ||
|
|
809274a294 | ||
|
|
544a17b0db | ||
|
|
3eddef40fb | ||
|
|
cb1b0550a2 | ||
|
|
5c9e9385e6 | ||
|
|
8dc0ad690b | ||
|
|
437d077bd6 | ||
|
|
53698040ab | ||
|
|
26e27e2686 | ||
|
|
924f2f6ea7 | ||
|
|
271f8bf78f | ||
|
|
03fc26215d | ||
|
|
87f70c66ba | ||
|
|
4b7a43fccd | ||
|
|
09fdd67b49 | ||
|
|
a22f89b687 | ||
|
|
d91c3572af | ||
|
|
585ceb96e4 | ||
|
|
86a4346fc9 | ||
|
|
1e7a0beaed | ||
|
|
d1890c69c9 | ||
|
|
7f467b0a0d | ||
|
|
c924d60aeb | ||
|
|
40da170f66 | ||
|
|
b9c4822c27 | ||
|
|
a890189530 | ||
|
|
04ce154d36 | ||
|
|
7cc2c0acef | ||
|
|
449418f537 | ||
|
|
952e5604d9 | ||
|
|
e55c97185a | ||
|
|
7d3196c3cf | ||
|
|
62c7202b33 | ||
|
|
ad23261478 | ||
|
|
e693857c39 | ||
|
|
bd39a92d16 | ||
|
|
b8bffe97fe | ||
|
|
1030f4ba75 | ||
|
|
61a3dc4033 | ||
|
|
48643582e9 | ||
|
|
108fdd9b7f | ||
|
|
3746b1cfad | ||
|
|
9b113c05c3 | ||
|
|
2672cb792c | ||
|
|
76181bafff | ||
|
|
d37ccd86ee | ||
|
|
64a26aabb8 | ||
|
|
9d8b196644 | ||
|
|
324d6c13b4 | ||
|
|
48a78c39e2 | ||
|
|
6f11891b1c | ||
|
|
29ee15e477 | ||
|
|
77f40a7201 | ||
|
|
ddbebd8f7d | ||
|
|
59d3e99ced | ||
|
|
5b7de4789c | ||
|
|
503ff07c9d | ||
|
|
d7f14fada4 | ||
|
|
0aa064916c | ||
|
|
9bdbefffe2 | ||
|
|
c2765f655c | ||
|
|
cab91e927e | ||
|
|
74e64c178d | ||
|
|
0a25b6bc8e | ||
|
|
033098038b | ||
|
|
174025eaff | ||
|
|
b4aba30ba8 | ||
|
|
ab9f482ffd | ||
|
|
4122cfca4c | ||
|
|
f8b10204f8 | ||
|
|
346e00d395 | ||
|
|
38b23d7fb0 | ||
|
|
21b1d79752 | ||
|
|
eda3ee8d3b | ||
|
|
dfd5524516 | ||
|
|
4ca64a85bc | ||
|
|
5aa34b898c | ||
|
|
257df891ee | ||
|
|
e2df1da5be | ||
|
|
50ea9daca6 | ||
|
|
9e16368222 | ||
|
|
baea0ecc92 | ||
|
|
b887940dce | ||
|
|
6e75e2b06c | ||
|
|
3d3fd6f724 | ||
|
|
05d3fd2a19 | ||
|
|
cec24e37c3 | ||
|
|
aed4c0ad17 | ||
|
|
426e19d1cc | ||
|
|
bf475776a9 | ||
|
|
08b555d724 | ||
|
|
d48f5b0b3f | ||
|
|
f074333c35 | ||
|
|
793f85e53f | ||
|
|
473429238f | ||
|
|
f4f949ebbc | ||
|
|
42c006fb2c | ||
|
|
673ec76fa7 | ||
|
|
bb81378b20 | ||
|
|
6785a1b854 | ||
|
|
c04699accf | ||
|
|
fce47ffed7 | ||
|
|
a185679cd1 | ||
|
|
1c290fa9d7 | ||
|
|
fb8aad52c4 | ||
|
|
adb15a7945 | ||
|
|
3d54723ec6 | ||
|
|
b7ed526cf3 | ||
|
|
fada218723 | ||
|
|
4a0825e27f | ||
|
|
7e03076a20 | ||
|
|
ce3c1528f9 | ||
|
|
a0e825c37e | ||
|
|
fb7a019786 | ||
|
|
1ab7cb62aa | ||
|
|
3d4a394ba9 | ||
|
|
d4598d7738 | ||
|
|
08cc026c1f | ||
|
|
f3c42f7f93 | ||
|
|
86bb04434d | ||
|
|
e3e0e397f0 | ||
|
|
ccbd7c155f | ||
|
|
c0884f53a6 | ||
|
|
d405dac854 | ||
|
|
d3427c7b19 | ||
|
|
9862636ab5 | ||
|
|
1d68caec37 | ||
|
|
b998cbd3e6 | ||
|
|
b504bf1617 | ||
|
|
afa60696c8 | ||
|
|
1afc99b648 | ||
|
|
083a531ccb | ||
|
|
1dfc7dc26e | ||
|
|
8eaa1a2d7e | ||
|
|
d706043c78 | ||
|
|
b8a2a3d613 | ||
|
|
ea7a143552 | ||
|
|
3f6d330f5f | ||
|
|
9d7b617cd6 | ||
|
|
75e04137de | ||
|
|
5e91aaa13e | ||
|
|
1e2fdc5f24 | ||
|
|
ad19107530 |
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,6 +6,26 @@ labels: "bug"
|
|||||||
assignees: ""
|
assignees: ""
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- If your issue is related to ICONS
|
||||||
|
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
||||||
|
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
||||||
|
- Follow the [THEMING](https://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
|
||||||
|
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
||||||
|
|
||||||
|
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
|
||||||
|
Quickshell does not ever update its DesktopEntires.
|
||||||
|
There is an open PR for it, that has been stuck unmerged over there to fix it.
|
||||||
|
We unfortunately are at the mercy of quickshell to merge it.
|
||||||
|
Until then, newly installed and removed apps will not react until the
|
||||||
|
shell is restarted.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Compositor
|
||||||
|
|
||||||
|
- [ ] niri
|
||||||
|
- [ ] Hyprland
|
||||||
|
- [ ] Other (specify)
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- Brief description of the issue -->
|
<!-- Brief description of the issue -->
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -14,6 +14,14 @@ assignees: ""
|
|||||||
|
|
||||||
<!-- Explain the purpose of this feature/why it'd be useful to you -->
|
<!-- Explain the purpose of this feature/why it'd be useful to you -->
|
||||||
|
|
||||||
|
## Compositor
|
||||||
|
|
||||||
|
Is this feature specific to one compositor?
|
||||||
|
|
||||||
|
- [ ] All compositors
|
||||||
|
- [ ] niri
|
||||||
|
- [ ] Hyprland
|
||||||
|
|
||||||
## Proposed Solution
|
## Proposed Solution
|
||||||
|
|
||||||
<!-- If you have any ideas for how to implement this, please share! -->
|
<!-- If you have any ideas for how to implement this, please share! -->
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/support_request.md
vendored
6
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -6,6 +6,12 @@ labels: "support"
|
|||||||
assignees: ""
|
assignees: ""
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Compositor
|
||||||
|
|
||||||
|
- [ ] niri
|
||||||
|
- [ ] Hyprland
|
||||||
|
- [ ] other
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- Brief description of the support needed -->
|
<!-- Brief description of the support needed -->
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -63,6 +63,11 @@ CLAUDE-temp.md
|
|||||||
niri-colors.generated.kdl
|
niri-colors.generated.kdl
|
||||||
ghostty-colors.generated.conf
|
ghostty-colors.generated.conf
|
||||||
|
|
||||||
|
# Notepad files (should be in ~/.local/state/DankMaterialShell/)
|
||||||
|
untitled-*.txt
|
||||||
|
file:*
|
||||||
|
notepad-files/
|
||||||
|
|
||||||
result
|
result
|
||||||
|
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
|||||||
20
CLAUDE.md
20
CLAUDE.md
@@ -64,8 +64,7 @@ quickshell -p shell.qml
|
|||||||
qs -p .
|
qs -p .
|
||||||
|
|
||||||
# Code formatting and linting
|
# Code formatting and linting
|
||||||
./qmlformat-all.sh # Format all QML files using project script
|
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
|
||||||
qmlformat -i **/*.qml # Format all QML files in place
|
|
||||||
qmllint **/*.qml # Lint all QML files for syntax errors
|
qmllint **/*.qml # Lint all QML files for syntax errors
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
│ ├── AudioService.qml
|
│ ├── AudioService.qml
|
||||||
│ ├── NetworkService.qml
|
│ ├── NetworkService.qml
|
||||||
│ ├── BluetoothService.qml
|
│ ├── BluetoothService.qml
|
||||||
│ ├── BrightnessService.qml
|
│ ├── DisplayService.qml
|
||||||
│ ├── NotificationService.qml
|
│ ├── NotificationService.qml
|
||||||
│ ├── WeatherService.qml
|
│ ├── WeatherService.qml
|
||||||
│ └── [14 more services]
|
│ └── [14 more services]
|
||||||
@@ -131,7 +130,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
3. **Services/** - System integration singletons
|
3. **Services/** - System integration singletons
|
||||||
- **Pattern**: All services use `Singleton` type with `id: root`
|
- **Pattern**: All services use `Singleton` type with `id: root`
|
||||||
- **Independence**: No cross-service dependencies
|
- **Independence**: No cross-service dependencies
|
||||||
- **Examples**: AudioService, NetworkService, BluetoothService, BrightnessService, WeatherService, NotificationService, CalendarService, BatteryService, NiriService, MprisController
|
- **Examples**: AudioService, NetworkService, BluetoothService, DisplayService, WeatherService, NotificationService, CalendarService, BatteryService, NiriService, MprisController
|
||||||
- Services handle system commands, state management, and hardware integration
|
- Services handle system commands, state management, and hardware integration
|
||||||
|
|
||||||
4. **Modules/** - UI components (93 files)
|
4. **Modules/** - UI components (93 files)
|
||||||
@@ -143,7 +142,6 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
- **ProcessList/**: System monitoring with process management and performance metrics
|
- **ProcessList/**: System monitoring with process management and performance metrics
|
||||||
- **Dock/**: Application dock with running apps and window management
|
- **Dock/**: Application dock with running apps and window management
|
||||||
- **Lock/**: Screen lock system with authentication
|
- **Lock/**: Screen lock system with authentication
|
||||||
- **CentcomCenter/**: Calendar, weather, and media widgets
|
|
||||||
|
|
||||||
5. **Modals/** - Full-screen overlays (10 files)
|
5. **Modals/** - Full-screen overlays (10 files)
|
||||||
- Modal system for settings, clipboard history, file browser, network info, power menu
|
- Modal system for settings, clipboard history, file browser, network info, power menu
|
||||||
@@ -221,6 +219,8 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
- Properties before signal handlers before child components
|
- Properties before signal handlers before child components
|
||||||
- Prefer property bindings over imperative code
|
- Prefer property bindings over imperative code
|
||||||
- **CRITICAL**: NEVER add comments unless absolutely essential for complex logic understanding. Code should be self-documenting through clear naming and structure. Comments are a code smell indicating unclear implementation.
|
- **CRITICAL**: NEVER add comments unless absolutely essential for complex logic understanding. Code should be self-documenting through clear naming and structure. Comments are a code smell indicating unclear implementation.
|
||||||
|
- Use guard statements, example `if (abc) { something() return;} somethingElse();`
|
||||||
|
- Don't use crazy ternary stuff, but use it for simple if else only. `propertyVal: a ? b : c`
|
||||||
|
|
||||||
2. **Naming Conventions**:
|
2. **Naming Conventions**:
|
||||||
- **Services**: Use `Singleton` type with `id: root`
|
- **Services**: Use `Singleton` type with `id: root`
|
||||||
@@ -228,9 +228,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
- **Properties**: camelCase for properties, PascalCase for types
|
- **Properties**: camelCase for properties, PascalCase for types
|
||||||
|
|
||||||
3. **Null-Safe Operations**:
|
3. **Null-Safe Operations**:
|
||||||
- **Do NOT use** `?.` operator (not supported by qmlformat)
|
- **Use** `object?.property`
|
||||||
- **Use** `object && object.property` instead of `object?.property`
|
|
||||||
- **Example**: `activePlayer && activePlayer.trackTitle` instead of `activePlayer?.trackTitle`
|
|
||||||
|
|
||||||
4. **Component Structure**:
|
4. **Component Structure**:
|
||||||
```qml
|
```qml
|
||||||
@@ -294,9 +292,9 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
|
|
||||||
// In modules - adapt UI accordingly
|
// In modules - adapt UI accordingly
|
||||||
DankSlider {
|
DankSlider {
|
||||||
visible: BrightnessService.brightnessAvailable
|
visible: DisplayService.brightnessAvailable
|
||||||
enabled: BrightnessService.brightnessAvailable
|
enabled: DisplayService.brightnessAvailable
|
||||||
value: BrightnessService.brightnessLevel
|
value: DisplayService.brightnessLevel
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
30
CONTRIBUTING.md
Normal file
30
CONTRIBUTING.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Contributions are welcome and encourages.
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported.
|
||||||
|
|
||||||
|
We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
|
||||||
|
|
||||||
|
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"customLocalFormatters.formatters": [
|
||||||
|
{
|
||||||
|
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
|
||||||
|
"languages": ["qml"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"[qml]": {
|
||||||
|
"editor.defaultFormatter": "jkillian.custom-local-formatters",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
|
||||||
|
|
||||||
|
## Pull request
|
||||||
|
|
||||||
|
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import QtQuick
|
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
QtObject {
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Singleton {
|
||||||
id: modalManager
|
id: modalManager
|
||||||
|
|
||||||
signal closeAllModalsExcept(var excludedModal)
|
signal closeAllModalsExcept(var excludedModal)
|
||||||
|
|||||||
@@ -45,4 +45,17 @@ Singleton {
|
|||||||
function copy(from: url, to: url): void {
|
function copy(from: url, to: url): void {
|
||||||
Quickshell.execDetached(["cp", strip(from), strip(to)])
|
Quickshell.execDetached(["cp", strip(from), strip(to)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
|
||||||
|
function moddedAppId(appId: string): string {
|
||||||
|
if (appId === "Spotify")
|
||||||
|
return "spotify-launcher"
|
||||||
|
if (appId === "beepertexts")
|
||||||
|
return "beeper"
|
||||||
|
if (appId === "home assistant desktop")
|
||||||
|
return "homeassistant-desktop"
|
||||||
|
if (appId.includes("com.transmissionbt.transmission"))
|
||||||
|
return "transmission-gtk"
|
||||||
|
return appId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import QtCore
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -15,9 +16,24 @@ Singleton {
|
|||||||
property string wallpaperPath: ""
|
property string wallpaperPath: ""
|
||||||
property string wallpaperLastPath: ""
|
property string wallpaperLastPath: ""
|
||||||
property string profileLastPath: ""
|
property string profileLastPath: ""
|
||||||
|
property bool perMonitorWallpaper: false
|
||||||
|
property var monitorWallpapers: ({})
|
||||||
property bool doNotDisturb: false
|
property bool doNotDisturb: false
|
||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
property int nightModeTemperature: 4500
|
property int nightModeTemperature: 4500
|
||||||
|
property bool nightModeAutoEnabled: false
|
||||||
|
property string nightModeAutoMode: "time"
|
||||||
|
|
||||||
|
property bool hasTriedDefaultSession: false
|
||||||
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
property int nightModeStartHour: 18
|
||||||
|
property int nightModeStartMinute: 0
|
||||||
|
property int nightModeEndHour: 6
|
||||||
|
property int nightModeEndMinute: 0
|
||||||
|
property real latitude: 0.0
|
||||||
|
property real longitude: 0.0
|
||||||
|
property string nightModeLocationProvider: ""
|
||||||
property var pinnedApps: []
|
property var pinnedApps: []
|
||||||
property int selectedGpuIndex: 0
|
property int selectedGpuIndex: 0
|
||||||
property bool nvidiaGpuTempEnabled: false
|
property bool nvidiaGpuTempEnabled: false
|
||||||
@@ -27,7 +43,27 @@ Singleton {
|
|||||||
property string wallpaperCyclingMode: "interval" // "interval" or "time"
|
property string wallpaperCyclingMode: "interval" // "interval" or "time"
|
||||||
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
|
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
|
||||||
property string wallpaperCyclingTime: "06:00" // HH:mm format
|
property string wallpaperCyclingTime: "06:00" // HH:mm format
|
||||||
|
property var monitorCyclingSettings: ({})
|
||||||
property string lastBrightnessDevice: ""
|
property string lastBrightnessDevice: ""
|
||||||
|
property string launchPrefix: ""
|
||||||
|
property string wallpaperTransition: "fade"
|
||||||
|
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
|
||||||
|
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
|
||||||
|
|
||||||
|
// Power management settings - AC Power
|
||||||
|
property int acMonitorTimeout: 0 // Never
|
||||||
|
property int acLockTimeout: 0 // Never
|
||||||
|
property int acSuspendTimeout: 0 // Never
|
||||||
|
property int acHibernateTimeout: 0 // Never
|
||||||
|
|
||||||
|
// Power management settings - Battery
|
||||||
|
property int batteryMonitorTimeout: 0 // Never
|
||||||
|
property int batteryLockTimeout: 0 // Never
|
||||||
|
property int batterySuspendTimeout: 0 // Never
|
||||||
|
property int batteryHibernateTimeout: 0 // Never
|
||||||
|
|
||||||
|
property bool lockBeforeSuspend: false
|
||||||
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
loadSettings()
|
loadSettings()
|
||||||
@@ -43,34 +79,64 @@ Singleton {
|
|||||||
var settings = JSON.parse(content)
|
var settings = JSON.parse(content)
|
||||||
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
|
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
|
||||||
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
|
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
|
||||||
wallpaperLastPath = settings.wallpaperLastPath
|
wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : ""
|
||||||
!== undefined ? settings.wallpaperLastPath : ""
|
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : ""
|
||||||
profileLastPath = settings.profileLastPath
|
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
|
||||||
!== undefined ? settings.profileLastPath : ""
|
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
|
||||||
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
|
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
|
||||||
nightModeEnabled = settings.nightModeEnabled
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
|
||||||
!== undefined ? settings.nightModeEnabled : false
|
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
|
||||||
nightModeTemperature = settings.nightModeTemperature
|
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
|
||||||
!== undefined ? settings.nightModeTemperature : 4500
|
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
|
||||||
|
// Handle legacy time format
|
||||||
|
if (settings.nightModeStartTime !== undefined) {
|
||||||
|
const parts = settings.nightModeStartTime.split(":")
|
||||||
|
nightModeStartHour = parseInt(parts[0]) || 18
|
||||||
|
nightModeStartMinute = parseInt(parts[1]) || 0
|
||||||
|
} else {
|
||||||
|
nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18
|
||||||
|
nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0
|
||||||
|
}
|
||||||
|
if (settings.nightModeEndTime !== undefined) {
|
||||||
|
const parts = settings.nightModeEndTime.split(":")
|
||||||
|
nightModeEndHour = parseInt(parts[0]) || 6
|
||||||
|
nightModeEndMinute = parseInt(parts[1]) || 0
|
||||||
|
} else {
|
||||||
|
nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6
|
||||||
|
nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0
|
||||||
|
}
|
||||||
|
latitude = settings.latitude !== undefined ? settings.latitude : 0.0
|
||||||
|
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
|
||||||
|
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
|
||||||
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
|
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
|
||||||
selectedGpuIndex = settings.selectedGpuIndex
|
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
||||||
!== undefined ? settings.selectedGpuIndex : 0
|
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
|
||||||
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled
|
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
||||||
!== undefined ? settings.nvidiaGpuTempEnabled : false
|
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
|
||||||
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled
|
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
|
||||||
!== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
|
||||||
enabledGpuPciIds = settings.enabledGpuPciIds
|
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
|
||||||
!== undefined ? settings.enabledGpuPciIds : []
|
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
|
||||||
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled
|
monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {}
|
||||||
!== undefined ? settings.wallpaperCyclingEnabled : false
|
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
|
||||||
wallpaperCyclingMode = settings.wallpaperCyclingMode
|
launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""
|
||||||
!== undefined ? settings.wallpaperCyclingMode : "interval"
|
wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"
|
||||||
wallpaperCyclingInterval = settings.wallpaperCyclingInterval
|
includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none")
|
||||||
!== undefined ? settings.wallpaperCyclingInterval : 300
|
|
||||||
wallpaperCyclingTime = settings.wallpaperCyclingTime
|
acMonitorTimeout = settings.acMonitorTimeout !== undefined ? settings.acMonitorTimeout : 0
|
||||||
!== undefined ? settings.wallpaperCyclingTime : "06:00"
|
acLockTimeout = settings.acLockTimeout !== undefined ? settings.acLockTimeout : 0
|
||||||
lastBrightnessDevice = settings.lastBrightnessDevice
|
acSuspendTimeout = settings.acSuspendTimeout !== undefined ? settings.acSuspendTimeout : 0
|
||||||
!== undefined ? settings.lastBrightnessDevice : ""
|
acHibernateTimeout = settings.acHibernateTimeout !== undefined ? settings.acHibernateTimeout : 0
|
||||||
|
batteryMonitorTimeout = settings.batteryMonitorTimeout !== undefined ? settings.batteryMonitorTimeout : 0
|
||||||
|
batteryLockTimeout = settings.batteryLockTimeout !== undefined ? settings.batteryLockTimeout : 0
|
||||||
|
batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0
|
||||||
|
batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0
|
||||||
|
lockBeforeSuspend = settings.lockBeforeSuspend !== undefined ? settings.lockBeforeSuspend : false
|
||||||
|
|
||||||
|
// Generate system themes but don't override user's theme choice
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
||||||
@@ -83,9 +149,20 @@ Singleton {
|
|||||||
"wallpaperPath": wallpaperPath,
|
"wallpaperPath": wallpaperPath,
|
||||||
"wallpaperLastPath": wallpaperLastPath,
|
"wallpaperLastPath": wallpaperLastPath,
|
||||||
"profileLastPath": profileLastPath,
|
"profileLastPath": profileLastPath,
|
||||||
|
"perMonitorWallpaper": perMonitorWallpaper,
|
||||||
|
"monitorWallpapers": monitorWallpapers,
|
||||||
"doNotDisturb": doNotDisturb,
|
"doNotDisturb": doNotDisturb,
|
||||||
"nightModeEnabled": nightModeEnabled,
|
"nightModeEnabled": nightModeEnabled,
|
||||||
"nightModeTemperature": nightModeTemperature,
|
"nightModeTemperature": nightModeTemperature,
|
||||||
|
"nightModeAutoEnabled": nightModeAutoEnabled,
|
||||||
|
"nightModeAutoMode": nightModeAutoMode,
|
||||||
|
"nightModeStartHour": nightModeStartHour,
|
||||||
|
"nightModeStartMinute": nightModeStartMinute,
|
||||||
|
"nightModeEndHour": nightModeEndHour,
|
||||||
|
"nightModeEndMinute": nightModeEndMinute,
|
||||||
|
"latitude": latitude,
|
||||||
|
"longitude": longitude,
|
||||||
|
"nightModeLocationProvider": nightModeLocationProvider,
|
||||||
"pinnedApps": pinnedApps,
|
"pinnedApps": pinnedApps,
|
||||||
"selectedGpuIndex": selectedGpuIndex,
|
"selectedGpuIndex": selectedGpuIndex,
|
||||||
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
|
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
|
||||||
@@ -95,7 +172,20 @@ Singleton {
|
|||||||
"wallpaperCyclingMode": wallpaperCyclingMode,
|
"wallpaperCyclingMode": wallpaperCyclingMode,
|
||||||
"wallpaperCyclingInterval": wallpaperCyclingInterval,
|
"wallpaperCyclingInterval": wallpaperCyclingInterval,
|
||||||
"wallpaperCyclingTime": wallpaperCyclingTime,
|
"wallpaperCyclingTime": wallpaperCyclingTime,
|
||||||
"lastBrightnessDevice": lastBrightnessDevice
|
"monitorCyclingSettings": monitorCyclingSettings,
|
||||||
|
"lastBrightnessDevice": lastBrightnessDevice,
|
||||||
|
"launchPrefix": launchPrefix,
|
||||||
|
"wallpaperTransition": wallpaperTransition,
|
||||||
|
"includedTransitions": includedTransitions,
|
||||||
|
"acMonitorTimeout": acMonitorTimeout,
|
||||||
|
"acLockTimeout": acLockTimeout,
|
||||||
|
"acSuspendTimeout": acSuspendTimeout,
|
||||||
|
"acHibernateTimeout": acHibernateTimeout,
|
||||||
|
"batteryMonitorTimeout": batteryMonitorTimeout,
|
||||||
|
"batteryLockTimeout": batteryLockTimeout,
|
||||||
|
"batterySuspendTimeout": batterySuspendTimeout,
|
||||||
|
"batteryHibernateTimeout": batteryHibernateTimeout,
|
||||||
|
"lockBeforeSuspend": lockBeforeSuspend
|
||||||
}, null, 2))
|
}, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +209,54 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setNightModeAutoEnabled(enabled) {
|
||||||
|
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
|
||||||
|
nightModeAutoEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeAutoMode(mode) {
|
||||||
|
nightModeAutoMode = mode
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeStartHour(hour) {
|
||||||
|
nightModeStartHour = hour
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeStartMinute(minute) {
|
||||||
|
nightModeStartMinute = minute
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeEndHour(hour) {
|
||||||
|
nightModeEndHour = hour
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeEndMinute(minute) {
|
||||||
|
nightModeEndMinute = minute
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLatitude(lat) {
|
||||||
|
console.log("SessionData: Setting latitude to", lat)
|
||||||
|
latitude = lat
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLongitude(lng) {
|
||||||
|
console.log("SessionData: Setting longitude to", lng)
|
||||||
|
longitude = lng
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeLocationProvider(provider) {
|
||||||
|
nightModeLocationProvider = provider
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
function setWallpaperPath(path) {
|
function setWallpaperPath(path) {
|
||||||
wallpaperPath = path
|
wallpaperPath = path
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -128,9 +266,36 @@ Singleton {
|
|||||||
wallpaperPath = imagePath
|
wallpaperPath = imagePath
|
||||||
saveSettings()
|
saveSettings()
|
||||||
|
|
||||||
if (typeof Theme !== "undefined" && typeof SettingsData !== "undefined"
|
if (typeof Theme !== "undefined") {
|
||||||
&& SettingsData.wallpaperDynamicTheming) {
|
if (Theme.currentTheme === Theme.dynamic) {
|
||||||
Theme.extractColors()
|
Theme.extractColors()
|
||||||
|
}
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperColor(color) {
|
||||||
|
wallpaperPath = color
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (Theme.currentTheme === Theme.dynamic) {
|
||||||
|
Theme.extractColors()
|
||||||
|
}
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWallpaper() {
|
||||||
|
wallpaperPath = ""
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
|
||||||
|
Theme.switchTheme(SettingsData.theme)
|
||||||
|
} else {
|
||||||
|
Theme.switchTheme("blue")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,40 +375,210 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMonitorCyclingSettings(screenName) {
|
||||||
|
return monitorCyclingSettings[screenName] || {
|
||||||
|
enabled: false,
|
||||||
|
mode: "interval",
|
||||||
|
interval: 300,
|
||||||
|
time: "06:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorCyclingEnabled(screenName, enabled) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
if (!newSettings[screenName]) {
|
||||||
|
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||||
|
}
|
||||||
|
newSettings[screenName].enabled = enabled
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorCyclingMode(screenName, mode) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
if (!newSettings[screenName]) {
|
||||||
|
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||||
|
}
|
||||||
|
newSettings[screenName].mode = mode
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorCyclingInterval(screenName, interval) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
if (!newSettings[screenName]) {
|
||||||
|
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||||
|
}
|
||||||
|
newSettings[screenName].interval = interval
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorCyclingTime(screenName, time) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
if (!newSettings[screenName]) {
|
||||||
|
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||||
|
}
|
||||||
|
newSettings[screenName].time = time
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPerMonitorWallpaper(enabled) {
|
||||||
|
perMonitorWallpaper = enabled
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
// Refresh dynamic theming when per-monitor mode changes
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (Theme.currentTheme === Theme.dynamic) {
|
||||||
|
Theme.extractColors()
|
||||||
|
}
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorWallpaper(screenName, path) {
|
||||||
|
var newMonitorWallpapers = Object.assign({}, monitorWallpapers)
|
||||||
|
if (path && path !== "") {
|
||||||
|
newMonitorWallpapers[screenName] = path
|
||||||
|
} else {
|
||||||
|
delete newMonitorWallpapers[screenName]
|
||||||
|
}
|
||||||
|
monitorWallpapers = newMonitorWallpapers
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
// Trigger dynamic theming if this is the first monitor and dynamic theming is enabled
|
||||||
|
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined") {
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0 && screenName === screens[0].name) {
|
||||||
|
if (Theme.currentTheme === Theme.dynamic) {
|
||||||
|
Theme.extractColors()
|
||||||
|
}
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonitorWallpaper(screenName) {
|
||||||
|
if (!perMonitorWallpaper) {
|
||||||
|
return wallpaperPath
|
||||||
|
}
|
||||||
|
return monitorWallpapers[screenName] || wallpaperPath
|
||||||
|
}
|
||||||
|
|
||||||
function setLastBrightnessDevice(device) {
|
function setLastBrightnessDevice(device) {
|
||||||
lastBrightnessDevice = device
|
lastBrightnessDevice = device
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setLaunchPrefix(prefix) {
|
||||||
|
launchPrefix = prefix
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperTransition(transition) {
|
||||||
|
wallpaperTransition = transition
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAcMonitorTimeout(timeout) {
|
||||||
|
acMonitorTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAcLockTimeout(timeout) {
|
||||||
|
acLockTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAcSuspendTimeout(timeout) {
|
||||||
|
acSuspendTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatteryMonitorTimeout(timeout) {
|
||||||
|
batteryMonitorTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatteryLockTimeout(timeout) {
|
||||||
|
batteryLockTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatterySuspendTimeout(timeout) {
|
||||||
|
batterySuspendTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAcHibernateTimeout(timeout) {
|
||||||
|
acHibernateTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatteryHibernateTimeout(timeout) {
|
||||||
|
batteryHibernateTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLockBeforeSuspend(enabled) {
|
||||||
|
lockBeforeSuspend = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: settingsFile
|
id: settingsFile
|
||||||
|
|
||||||
path: StandardPaths.writableLocation(
|
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||||
StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
parseSettings(settingsFile.text())
|
parseSettings(settingsFile.text())
|
||||||
|
hasTriedDefaultSession = false
|
||||||
|
}
|
||||||
|
onLoadFailed: error => {
|
||||||
|
if (!hasTriedDefaultSession) {
|
||||||
|
hasTriedDefaultSession = true
|
||||||
|
defaultSessionCheckProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: defaultSessionCheckProcess
|
||||||
|
|
||||||
|
command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir
|
||||||
|
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"]
|
||||||
|
running: false
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
console.log("Copied default-session.json to session.json")
|
||||||
|
settingsFile.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onLoadFailed: error => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "wallpaper"
|
target: "wallpaper"
|
||||||
|
|
||||||
function get(): string {
|
function get(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead."
|
||||||
|
}
|
||||||
return root.wallpaperPath || ""
|
return root.wallpaperPath || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function set(path: string): string {
|
function set(path: string): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead."
|
||||||
|
}
|
||||||
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return "ERROR: No path provided"
|
return "ERROR: No path provided"
|
||||||
}
|
}
|
||||||
|
|
||||||
var absolutePath = path.startsWith(
|
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||||
"/") ? path : StandardPaths.writableLocation(
|
|
||||||
StandardPaths.HomeLocation) + "/" + path
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
root.setWallpaper(absolutePath)
|
root.setWallpaper(absolutePath)
|
||||||
@@ -255,10 +590,17 @@ Singleton {
|
|||||||
|
|
||||||
function clear(): string {
|
function clear(): string {
|
||||||
root.setWallpaper("")
|
root.setWallpaper("")
|
||||||
return "SUCCESS: Wallpaper cleared"
|
root.setPerMonitorWallpaper(false)
|
||||||
|
root.monitorWallpapers = {}
|
||||||
|
root.saveSettings()
|
||||||
|
return "SUCCESS: All wallpapers cleared"
|
||||||
}
|
}
|
||||||
|
|
||||||
function next(): string {
|
function next(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead."
|
||||||
|
}
|
||||||
|
|
||||||
if (!root.wallpaperPath) {
|
if (!root.wallpaperPath) {
|
||||||
return "ERROR: No wallpaper set"
|
return "ERROR: No wallpaper set"
|
||||||
}
|
}
|
||||||
@@ -272,6 +614,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prev(): string {
|
function prev(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead."
|
||||||
|
}
|
||||||
|
|
||||||
if (!root.wallpaperPath) {
|
if (!root.wallpaperPath) {
|
||||||
return "ERROR: No wallpaper set"
|
return "ERROR: No wallpaper set"
|
||||||
}
|
}
|
||||||
@@ -283,28 +629,70 @@ Singleton {
|
|||||||
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
function getFor(screenName: string): string {
|
||||||
target: "theme"
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
function toggle(): string {
|
}
|
||||||
root.setLightMode(!root.isLightMode)
|
return root.getMonitorWallpaper(screenName) || ""
|
||||||
return root.isLightMode ? "light" : "dark"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function light(): string {
|
function setFor(screenName: string, path: string): string {
|
||||||
root.setLightMode(true)
|
if (!screenName) {
|
||||||
return "light"
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return "ERROR: No path provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!root.perMonitorWallpaper) {
|
||||||
|
root.setPerMonitorWallpaper(true)
|
||||||
|
}
|
||||||
|
root.setMonitorWallpaper(screenName, absolutePath)
|
||||||
|
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dark(): string {
|
function nextFor(screenName: string): string {
|
||||||
root.setLightMode(false)
|
if (!screenName) {
|
||||||
return "dark"
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||||
|
if (!currentWallpaper) {
|
||||||
|
return "ERROR: No wallpaper set for " + screenName
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cycleNextForMonitor(screenName)
|
||||||
|
return "SUCCESS: Cycling to next wallpaper for " + screenName
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMode(): string {
|
function prevFor(screenName: string): string {
|
||||||
return root.isLightMode ? "light" : "dark"
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||||
|
if (!currentWallpaper) {
|
||||||
|
return "ERROR: No wallpaper set for " + screenName
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cyclePrevForMonitor(screenName)
|
||||||
|
return "SUCCESS: Cycling to previous wallpaper for " + screenName
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtCore
|
import QtCore
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -13,6 +15,7 @@ Singleton {
|
|||||||
// Theme settings
|
// Theme settings
|
||||||
property string currentThemeName: "blue"
|
property string currentThemeName: "blue"
|
||||||
property string customThemeFile: ""
|
property string customThemeFile: ""
|
||||||
|
property string matugenScheme: "scheme-tonal-spot"
|
||||||
property real topBarTransparency: 0.75
|
property real topBarTransparency: 0.75
|
||||||
property real topBarWidgetTransparency: 0.85
|
property real topBarWidgetTransparency: 0.85
|
||||||
property real popupTransparency: 0.92
|
property real popupTransparency: 0.92
|
||||||
@@ -43,14 +46,29 @@ Singleton {
|
|||||||
property bool controlCenterShowNetworkIcon: true
|
property bool controlCenterShowNetworkIcon: true
|
||||||
property bool controlCenterShowBluetoothIcon: true
|
property bool controlCenterShowBluetoothIcon: true
|
||||||
property bool controlCenterShowAudioIcon: true
|
property bool controlCenterShowAudioIcon: true
|
||||||
|
property var controlCenterWidgets: [
|
||||||
|
{"id": "volumeSlider", "enabled": true, "width": 50},
|
||||||
|
{"id": "brightnessSlider", "enabled": true, "width": 50},
|
||||||
|
{"id": "wifi", "enabled": true, "width": 50},
|
||||||
|
{"id": "bluetooth", "enabled": true, "width": 50},
|
||||||
|
{"id": "audioOutput", "enabled": true, "width": 50},
|
||||||
|
{"id": "audioInput", "enabled": true, "width": 50},
|
||||||
|
{"id": "nightMode", "enabled": true, "width": 50},
|
||||||
|
{"id": "darkMode", "enabled": true, "width": 50}
|
||||||
|
]
|
||||||
property bool showWorkspaceIndex: false
|
property bool showWorkspaceIndex: false
|
||||||
property bool showWorkspacePadding: false
|
property bool showWorkspacePadding: false
|
||||||
|
property bool showWorkspaceApps: false
|
||||||
|
property int maxWorkspaceIcons: 3
|
||||||
|
property bool workspacesPerMonitor: true
|
||||||
property var workspaceNameIcons: ({})
|
property var workspaceNameIcons: ({})
|
||||||
|
property bool waveProgressEnabled: true
|
||||||
property bool clockCompactMode: false
|
property bool clockCompactMode: false
|
||||||
property bool focusedWindowCompactMode: false
|
property bool focusedWindowCompactMode: false
|
||||||
property bool runningAppsCompactMode: true
|
property bool runningAppsCompactMode: true
|
||||||
property string clockDateFormat: "ddd d"
|
property bool runningAppsCurrentWorkspace: false
|
||||||
property string lockDateFormat: "dddd, MMMM d"
|
property string clockDateFormat: ""
|
||||||
|
property string lockDateFormat: ""
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
property var topBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
property var topBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
||||||
property var topBarCenterWidgets: ["music", "clock", "weather"]
|
property var topBarCenterWidgets: ["music", "clock", "weather"]
|
||||||
@@ -71,48 +89,109 @@ Singleton {
|
|||||||
property string osLogoColorOverride: ""
|
property string osLogoColorOverride: ""
|
||||||
property real osLogoBrightness: 0.5
|
property real osLogoBrightness: 0.5
|
||||||
property real osLogoContrast: 1
|
property real osLogoContrast: 1
|
||||||
property bool wallpaperDynamicTheming: true
|
|
||||||
property bool weatherEnabled: true
|
property bool weatherEnabled: true
|
||||||
property string fontFamily: "Inter Variable"
|
property string fontFamily: "Inter Variable"
|
||||||
property string monoFontFamily: "Fira Code"
|
property string monoFontFamily: "Fira Code"
|
||||||
property int fontWeight: Font.Normal
|
property int fontWeight: Font.Normal
|
||||||
|
property real fontScale: 1.0
|
||||||
|
property bool notepadUseMonospace: true
|
||||||
|
property string notepadFontFamily: ""
|
||||||
|
property real notepadFontSize: 14
|
||||||
|
property bool notepadShowLineNumbers: false
|
||||||
|
property real notepadTransparencyOverride: -1
|
||||||
|
property real notepadLastCustomTransparency: 0.7
|
||||||
|
|
||||||
|
onNotepadUseMonospaceChanged: saveSettings()
|
||||||
|
onNotepadFontFamilyChanged: saveSettings()
|
||||||
|
onNotepadFontSizeChanged: saveSettings()
|
||||||
|
onNotepadShowLineNumbersChanged: saveSettings()
|
||||||
|
onNotepadTransparencyOverrideChanged: {
|
||||||
|
if (notepadTransparencyOverride > 0) {
|
||||||
|
notepadLastCustomTransparency = notepadTransparencyOverride
|
||||||
|
}
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
onNotepadLastCustomTransparencyChanged: saveSettings()
|
||||||
property bool gtkThemingEnabled: false
|
property bool gtkThemingEnabled: false
|
||||||
property bool qtThemingEnabled: false
|
property bool qtThemingEnabled: false
|
||||||
property bool showDock: false
|
property bool showDock: false
|
||||||
property bool dockAutoHide: false
|
property bool dockAutoHide: false
|
||||||
|
property bool dockGroupByApp: false
|
||||||
property real cornerRadius: 12
|
property real cornerRadius: 12
|
||||||
property bool notificationOverlayEnabled: false
|
property bool notificationOverlayEnabled: false
|
||||||
property bool topBarAutoHide: false
|
property bool topBarAutoHide: false
|
||||||
|
property bool topBarOpenOnOverview: false
|
||||||
|
property bool topBarVisible: true
|
||||||
property real topBarSpacing: 4
|
property real topBarSpacing: 4
|
||||||
|
property real topBarBottomGap: 0
|
||||||
|
property real topBarInnerPadding: 8
|
||||||
property bool topBarSquareCorners: false
|
property bool topBarSquareCorners: false
|
||||||
|
property bool topBarNoBackground: false
|
||||||
|
property bool topBarGothCornersEnabled: false
|
||||||
|
property bool lockScreenShowPowerActions: true
|
||||||
|
property bool hideBrightnessSlider: false
|
||||||
|
property string widgetBackgroundColor: "sch"
|
||||||
|
property string surfaceBase: "sc"
|
||||||
property int notificationTimeoutLow: 5000
|
property int notificationTimeoutLow: 5000
|
||||||
property int notificationTimeoutNormal: 5000
|
property int notificationTimeoutNormal: 5000
|
||||||
property int notificationTimeoutCritical: 0
|
property int notificationTimeoutCritical: 0
|
||||||
|
property var screenPreferences: ({})
|
||||||
readonly property string defaultFontFamily: "Inter Variable"
|
readonly property string defaultFontFamily: "Inter Variable"
|
||||||
readonly property string defaultMonoFontFamily: "Fira Code"
|
readonly property string defaultMonoFontFamily: "Fira Code"
|
||||||
readonly property string _homeUrl: StandardPaths.writableLocation(
|
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||||
StandardPaths.HomeLocation)
|
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||||
readonly property string _configUrl: StandardPaths.writableLocation(
|
readonly property string _configDir: Paths.strip(_configUrl)
|
||||||
StandardPaths.ConfigLocation)
|
|
||||||
readonly property string _configDir: _configUrl.startsWith(
|
|
||||||
"file://") ? _configUrl.substring(
|
|
||||||
7) : _configUrl
|
|
||||||
|
|
||||||
signal forceTopBarLayoutRefresh
|
signal forceTopBarLayoutRefresh
|
||||||
signal widgetDataChanged
|
signal widgetDataChanged
|
||||||
signal workspaceIconsUpdated
|
signal workspaceIconsUpdated
|
||||||
|
|
||||||
|
property bool _loading: false
|
||||||
|
|
||||||
|
function getEffectiveTimeFormat() {
|
||||||
|
if (use24HourClock) {
|
||||||
|
return Locale.ShortFormat
|
||||||
|
} else {
|
||||||
|
return "h:mm AP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveClockDateFormat() {
|
||||||
|
return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveLockDateFormat() {
|
||||||
|
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat
|
||||||
|
}
|
||||||
|
|
||||||
function initializeListModels() {
|
function initializeListModels() {
|
||||||
|
// ! Hack-ish to add all properties to the listmodel once
|
||||||
|
// ! allows the properties to be bound on new widget addtions
|
||||||
|
var dummyItem = {
|
||||||
|
"widgetId": "dummy",
|
||||||
|
"enabled": true,
|
||||||
|
"size": 20,
|
||||||
|
"selectedGpuIndex": 0,
|
||||||
|
"pciId": "",
|
||||||
|
"mountPath": "/"
|
||||||
|
}
|
||||||
|
leftWidgetsModel.append(dummyItem)
|
||||||
|
centerWidgetsModel.append(dummyItem)
|
||||||
|
rightWidgetsModel.append(dummyItem)
|
||||||
|
|
||||||
updateListModel(leftWidgetsModel, topBarLeftWidgets)
|
updateListModel(leftWidgetsModel, topBarLeftWidgets)
|
||||||
updateListModel(centerWidgetsModel, topBarCenterWidgets)
|
updateListModel(centerWidgetsModel, topBarCenterWidgets)
|
||||||
updateListModel(rightWidgetsModel, topBarRightWidgets)
|
updateListModel(rightWidgetsModel, topBarRightWidgets)
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
|
_loading = true
|
||||||
parseSettings(settingsFile.text())
|
parseSettings(settingsFile.text())
|
||||||
|
_loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSettings(content) {
|
function parseSettings(content) {
|
||||||
|
_loading = true
|
||||||
try {
|
try {
|
||||||
if (content && content.trim()) {
|
if (content && content.trim()) {
|
||||||
var settings = JSON.parse(content)
|
var settings = JSON.parse(content)
|
||||||
@@ -129,42 +208,21 @@ Singleton {
|
|||||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue"
|
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue"
|
||||||
}
|
}
|
||||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""
|
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""
|
||||||
topBarTransparency = settings.topBarTransparency
|
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"
|
||||||
!== undefined ? (settings.topBarTransparency
|
topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 0.75
|
||||||
> 1 ? settings.topBarTransparency
|
topBarWidgetTransparency = settings.topBarWidgetTransparency !== undefined ? (settings.topBarWidgetTransparency > 1 ? settings.topBarWidgetTransparency / 100 : settings.topBarWidgetTransparency) : 0.85
|
||||||
/ 100 : settings.topBarTransparency) : 0.75
|
popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 0.92
|
||||||
topBarWidgetTransparency = settings.topBarWidgetTransparency
|
dockTransparency = settings.dockTransparency !== undefined ? (settings.dockTransparency > 1 ? settings.dockTransparency / 100 : settings.dockTransparency) : 1
|
||||||
!== undefined ? (settings.topBarWidgetTransparency
|
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true
|
||||||
> 1 ? settings.topBarWidgetTransparency
|
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false
|
||||||
/ 100 : settings.topBarWidgetTransparency) : 0.85
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
|
||||||
popupTransparency = settings.popupTransparency
|
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY"
|
||||||
!== undefined ? (settings.popupTransparency
|
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060"
|
||||||
> 1 ? settings.popupTransparency
|
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false
|
||||||
/ 100 : settings.popupTransparency) : 0.92
|
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true
|
||||||
dockTransparency = settings.dockTransparency
|
showLauncherButton = settings.showLauncherButton !== undefined ? settings.showLauncherButton : true
|
||||||
!== undefined ? (settings.dockTransparency
|
showWorkspaceSwitcher = settings.showWorkspaceSwitcher !== undefined ? settings.showWorkspaceSwitcher : true
|
||||||
> 1 ? settings.dockTransparency
|
showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true
|
||||||
/ 100 : settings.dockTransparency) : 1
|
|
||||||
use24HourClock = settings.use24HourClock
|
|
||||||
!== undefined ? settings.use24HourClock : true
|
|
||||||
useFahrenheit = settings.useFahrenheit
|
|
||||||
!== undefined ? settings.useFahrenheit : false
|
|
||||||
nightModeEnabled = settings.nightModeEnabled
|
|
||||||
!== undefined ? settings.nightModeEnabled : false
|
|
||||||
weatherLocation = settings.weatherLocation
|
|
||||||
!== undefined ? settings.weatherLocation : "New York, NY"
|
|
||||||
weatherCoordinates = settings.weatherCoordinates
|
|
||||||
!== undefined ? settings.weatherCoordinates : "40.7128,-74.0060"
|
|
||||||
useAutoLocation = settings.useAutoLocation
|
|
||||||
!== undefined ? settings.useAutoLocation : false
|
|
||||||
weatherEnabled = settings.weatherEnabled
|
|
||||||
!== undefined ? settings.weatherEnabled : true
|
|
||||||
showLauncherButton = settings.showLauncherButton
|
|
||||||
!== undefined ? settings.showLauncherButton : true
|
|
||||||
showWorkspaceSwitcher = settings.showWorkspaceSwitcher
|
|
||||||
!== undefined ? settings.showWorkspaceSwitcher : true
|
|
||||||
showFocusedWindow = settings.showFocusedWindow
|
|
||||||
!== undefined ? settings.showFocusedWindow : true
|
|
||||||
showWeather = settings.showWeather !== undefined ? settings.showWeather : true
|
showWeather = settings.showWeather !== undefined ? settings.showWeather : true
|
||||||
showMusic = settings.showMusic !== undefined ? settings.showMusic : true
|
showMusic = settings.showMusic !== undefined ? settings.showMusic : true
|
||||||
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true
|
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true
|
||||||
@@ -172,62 +230,54 @@ Singleton {
|
|||||||
showMemUsage = settings.showMemUsage !== undefined ? settings.showMemUsage : true
|
showMemUsage = settings.showMemUsage !== undefined ? settings.showMemUsage : true
|
||||||
showCpuTemp = settings.showCpuTemp !== undefined ? settings.showCpuTemp : true
|
showCpuTemp = settings.showCpuTemp !== undefined ? settings.showCpuTemp : true
|
||||||
showGpuTemp = settings.showGpuTemp !== undefined ? settings.showGpuTemp : true
|
showGpuTemp = settings.showGpuTemp !== undefined ? settings.showGpuTemp : true
|
||||||
selectedGpuIndex = settings.selectedGpuIndex
|
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
||||||
!== undefined ? settings.selectedGpuIndex : 0
|
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
|
||||||
enabledGpuPciIds = settings.enabledGpuPciIds
|
showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true
|
||||||
!== undefined ? settings.enabledGpuPciIds : []
|
|
||||||
showSystemTray = settings.showSystemTray
|
|
||||||
!== undefined ? settings.showSystemTray : true
|
|
||||||
showClock = settings.showClock !== undefined ? settings.showClock : true
|
showClock = settings.showClock !== undefined ? settings.showClock : true
|
||||||
showNotificationButton = settings.showNotificationButton
|
showNotificationButton = settings.showNotificationButton !== undefined ? settings.showNotificationButton : true
|
||||||
!== undefined ? settings.showNotificationButton : true
|
|
||||||
showBattery = settings.showBattery !== undefined ? settings.showBattery : true
|
showBattery = settings.showBattery !== undefined ? settings.showBattery : true
|
||||||
showControlCenterButton = settings.showControlCenterButton
|
showControlCenterButton = settings.showControlCenterButton !== undefined ? settings.showControlCenterButton : true
|
||||||
!== undefined ? settings.showControlCenterButton : true
|
controlCenterShowNetworkIcon = settings.controlCenterShowNetworkIcon !== undefined ? settings.controlCenterShowNetworkIcon : true
|
||||||
controlCenterShowNetworkIcon = settings.controlCenterShowNetworkIcon
|
controlCenterShowBluetoothIcon = settings.controlCenterShowBluetoothIcon !== undefined ? settings.controlCenterShowBluetoothIcon : true
|
||||||
!== undefined ? settings.controlCenterShowNetworkIcon : true
|
controlCenterShowAudioIcon = settings.controlCenterShowAudioIcon !== undefined ? settings.controlCenterShowAudioIcon : true
|
||||||
controlCenterShowBluetoothIcon = settings.controlCenterShowBluetoothIcon
|
controlCenterWidgets = settings.controlCenterWidgets !== undefined ? settings.controlCenterWidgets : [
|
||||||
!== undefined ? settings.controlCenterShowBluetoothIcon : true
|
{"id": "volumeSlider", "enabled": true, "width": 50},
|
||||||
controlCenterShowAudioIcon = settings.controlCenterShowAudioIcon
|
{"id": "brightnessSlider", "enabled": true, "width": 50},
|
||||||
!== undefined ? settings.controlCenterShowAudioIcon : true
|
{"id": "wifi", "enabled": true, "width": 50},
|
||||||
showWorkspaceIndex = settings.showWorkspaceIndex
|
{"id": "bluetooth", "enabled": true, "width": 50},
|
||||||
!== undefined ? settings.showWorkspaceIndex : false
|
{"id": "audioOutput", "enabled": true, "width": 50},
|
||||||
showWorkspacePadding = settings.showWorkspacePadding
|
{"id": "audioInput", "enabled": true, "width": 50},
|
||||||
!== undefined ? settings.showWorkspacePadding : false
|
{"id": "nightMode", "enabled": true, "width": 50},
|
||||||
workspaceNameIcons = settings.workspaceNameIcons
|
{"id": "darkMode", "enabled": true, "width": 50}
|
||||||
!== undefined ? settings.workspaceNameIcons : ({})
|
]
|
||||||
clockCompactMode = settings.clockCompactMode
|
showWorkspaceIndex = settings.showWorkspaceIndex !== undefined ? settings.showWorkspaceIndex : false
|
||||||
!== undefined ? settings.clockCompactMode : false
|
showWorkspacePadding = settings.showWorkspacePadding !== undefined ? settings.showWorkspacePadding : false
|
||||||
focusedWindowCompactMode = settings.focusedWindowCompactMode
|
showWorkspaceApps = settings.showWorkspaceApps !== undefined ? settings.showWorkspaceApps : false
|
||||||
!== undefined ? settings.focusedWindowCompactMode : false
|
maxWorkspaceIcons = settings.maxWorkspaceIcons !== undefined ? settings.maxWorkspaceIcons : 3
|
||||||
runningAppsCompactMode = settings.runningAppsCompactMode
|
workspaceNameIcons = settings.workspaceNameIcons !== undefined ? settings.workspaceNameIcons : ({})
|
||||||
!== undefined ? settings.runningAppsCompactMode : true
|
workspacesPerMonitor = settings.workspacesPerMonitor !== undefined ? settings.workspacesPerMonitor : true
|
||||||
clockDateFormat = settings.clockDateFormat
|
waveProgressEnabled = settings.waveProgressEnabled !== undefined ? settings.waveProgressEnabled : true
|
||||||
!== undefined ? settings.clockDateFormat : "ddd d"
|
clockCompactMode = settings.clockCompactMode !== undefined ? settings.clockCompactMode : false
|
||||||
lockDateFormat = settings.lockDateFormat
|
focusedWindowCompactMode = settings.focusedWindowCompactMode !== undefined ? settings.focusedWindowCompactMode : false
|
||||||
!== undefined ? settings.lockDateFormat : "dddd, MMMM d"
|
runningAppsCompactMode = settings.runningAppsCompactMode !== undefined ? settings.runningAppsCompactMode : true
|
||||||
|
runningAppsCurrentWorkspace = settings.runningAppsCurrentWorkspace !== undefined ? settings.runningAppsCurrentWorkspace : false
|
||||||
|
clockDateFormat = settings.clockDateFormat !== undefined ? settings.clockDateFormat : ""
|
||||||
|
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : ""
|
||||||
mediaSize = settings.mediaSize !== undefined ? settings.mediaSize : (settings.mediaCompactMode !== undefined ? (settings.mediaCompactMode ? 0 : 1) : 1)
|
mediaSize = settings.mediaSize !== undefined ? settings.mediaSize : (settings.mediaCompactMode !== undefined ? (settings.mediaCompactMode ? 0 : 1) : 1)
|
||||||
if (settings.topBarWidgetOrder) {
|
if (settings.topBarWidgetOrder) {
|
||||||
topBarLeftWidgets = settings.topBarWidgetOrder.filter(w => {
|
topBarLeftWidgets = settings.topBarWidgetOrder.filter(w => {
|
||||||
return ["launcherButton", "workspaceSwitcher", "focusedWindow"].includes(w)
|
return ["launcherButton", "workspaceSwitcher", "focusedWindow"].includes(w)
|
||||||
})
|
})
|
||||||
topBarCenterWidgets = settings.topBarWidgetOrder.filter(
|
topBarCenterWidgets = settings.topBarWidgetOrder.filter(w => {
|
||||||
w => {
|
return ["clock", "music", "weather"].includes(w)
|
||||||
return ["clock", "music", "weather"].includes(
|
})
|
||||||
w)
|
topBarRightWidgets = settings.topBarWidgetOrder.filter(w => {
|
||||||
})
|
return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].includes(w)
|
||||||
topBarRightWidgets = settings.topBarWidgetOrder.filter(
|
})
|
||||||
w => {
|
|
||||||
return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].includes(
|
|
||||||
w)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
var leftWidgets = settings.topBarLeftWidgets
|
var leftWidgets = settings.topBarLeftWidgets !== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
||||||
!== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
var centerWidgets = settings.topBarCenterWidgets !== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"]
|
||||||
var centerWidgets = settings.topBarCenterWidgets
|
var rightWidgets = settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
|
||||||
!== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"]
|
|
||||||
var rightWidgets = settings.topBarRightWidgets
|
|
||||||
!== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
|
|
||||||
topBarLeftWidgets = leftWidgets
|
topBarLeftWidgets = leftWidgets
|
||||||
topBarCenterWidgets = centerWidgets
|
topBarCenterWidgets = centerWidgets
|
||||||
topBarRightWidgets = rightWidgets
|
topBarRightWidgets = rightWidgets
|
||||||
@@ -235,46 +285,48 @@ Singleton {
|
|||||||
updateListModel(centerWidgetsModel, centerWidgets)
|
updateListModel(centerWidgetsModel, centerWidgets)
|
||||||
updateListModel(rightWidgetsModel, rightWidgets)
|
updateListModel(rightWidgetsModel, rightWidgets)
|
||||||
}
|
}
|
||||||
appLauncherViewMode = settings.appLauncherViewMode
|
appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list"
|
||||||
!== undefined ? settings.appLauncherViewMode : "list"
|
spotlightModalViewMode = settings.spotlightModalViewMode !== undefined ? settings.spotlightModalViewMode : "list"
|
||||||
spotlightModalViewMode = settings.spotlightModalViewMode
|
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto"
|
||||||
!== undefined ? settings.spotlightModalViewMode : "list"
|
|
||||||
networkPreference = settings.networkPreference
|
|
||||||
!== undefined ? settings.networkPreference : "auto"
|
|
||||||
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default"
|
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default"
|
||||||
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false
|
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false
|
||||||
osLogoColorOverride = settings.osLogoColorOverride
|
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : ""
|
||||||
!== undefined ? settings.osLogoColorOverride : ""
|
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5
|
||||||
osLogoBrightness = settings.osLogoBrightness
|
|
||||||
!== undefined ? settings.osLogoBrightness : 0.5
|
|
||||||
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1
|
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1
|
||||||
wallpaperDynamicTheming = settings.wallpaperDynamicTheming
|
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : defaultFontFamily
|
||||||
!== undefined ? settings.wallpaperDynamicTheming : true
|
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : defaultMonoFontFamily
|
||||||
fontFamily = settings.fontFamily
|
|
||||||
!== undefined ? settings.fontFamily : defaultFontFamily
|
|
||||||
monoFontFamily = settings.monoFontFamily
|
|
||||||
!== undefined ? settings.monoFontFamily : defaultMonoFontFamily
|
|
||||||
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal
|
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal
|
||||||
gtkThemingEnabled = settings.gtkThemingEnabled
|
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0
|
||||||
!== undefined ? settings.gtkThemingEnabled : false
|
notepadUseMonospace = settings.notepadUseMonospace !== undefined ? settings.notepadUseMonospace : true
|
||||||
qtThemingEnabled = settings.qtThemingEnabled
|
notepadFontFamily = settings.notepadFontFamily !== undefined ? settings.notepadFontFamily : ""
|
||||||
!== undefined ? settings.qtThemingEnabled : false
|
notepadFontSize = settings.notepadFontSize !== undefined ? settings.notepadFontSize : 14
|
||||||
|
notepadShowLineNumbers = settings.notepadShowLineNumbers !== undefined ? settings.notepadShowLineNumbers : false
|
||||||
|
notepadTransparencyOverride = settings.notepadTransparencyOverride !== undefined ? settings.notepadTransparencyOverride : -1
|
||||||
|
notepadLastCustomTransparency = settings.notepadLastCustomTransparency !== undefined ? settings.notepadLastCustomTransparency : 0.95
|
||||||
|
gtkThemingEnabled = settings.gtkThemingEnabled !== undefined ? settings.gtkThemingEnabled : false
|
||||||
|
qtThemingEnabled = settings.qtThemingEnabled !== undefined ? settings.qtThemingEnabled : false
|
||||||
showDock = settings.showDock !== undefined ? settings.showDock : false
|
showDock = settings.showDock !== undefined ? settings.showDock : false
|
||||||
dockAutoHide = settings.dockAutoHide !== undefined ? settings.dockAutoHide : false
|
dockAutoHide = settings.dockAutoHide !== undefined ? settings.dockAutoHide : false
|
||||||
|
dockGroupByApp = settings.dockGroupByApp !== undefined ? settings.dockGroupByApp : false
|
||||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12
|
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12
|
||||||
notificationOverlayEnabled = settings.notificationOverlayEnabled
|
notificationOverlayEnabled = settings.notificationOverlayEnabled !== undefined ? settings.notificationOverlayEnabled : false
|
||||||
!== undefined ? settings.notificationOverlayEnabled : false
|
topBarAutoHide = settings.topBarAutoHide !== undefined ? settings.topBarAutoHide : false
|
||||||
topBarAutoHide = settings.topBarAutoHide
|
topBarOpenOnOverview = settings.topBarOpenOnOverview !== undefined ? settings.topBarOpenOnOverview : false
|
||||||
!== undefined ? settings.topBarAutoHide : false
|
topBarVisible = settings.topBarVisible !== undefined ? settings.topBarVisible : true
|
||||||
notificationTimeoutLow = settings.notificationTimeoutLow
|
notificationTimeoutLow = settings.notificationTimeoutLow !== undefined ? settings.notificationTimeoutLow : 5000
|
||||||
!== undefined ? settings.notificationTimeoutLow : 5000
|
notificationTimeoutNormal = settings.notificationTimeoutNormal !== undefined ? settings.notificationTimeoutNormal : 5000
|
||||||
notificationTimeoutNormal = settings.notificationTimeoutNormal
|
notificationTimeoutCritical = settings.notificationTimeoutCritical !== undefined ? settings.notificationTimeoutCritical : 0
|
||||||
!== undefined ? settings.notificationTimeoutNormal : 5000
|
|
||||||
notificationTimeoutCritical = settings.notificationTimeoutCritical
|
|
||||||
!== undefined ? settings.notificationTimeoutCritical : 0
|
|
||||||
topBarSpacing = settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4
|
topBarSpacing = settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4
|
||||||
topBarSquareCorners = settings.topBarSquareCorners
|
topBarBottomGap = settings.topBarBottomGap !== undefined ? settings.topBarBottomGap : 0
|
||||||
!== undefined ? settings.topBarSquareCorners : false
|
topBarInnerPadding = settings.topBarInnerPadding !== undefined ? settings.topBarInnerPadding : 8
|
||||||
|
topBarSquareCorners = settings.topBarSquareCorners !== undefined ? settings.topBarSquareCorners : false
|
||||||
|
topBarNoBackground = settings.topBarNoBackground !== undefined ? settings.topBarNoBackground : false
|
||||||
|
topBarGothCornersEnabled = settings.topBarGothCornersEnabled !== undefined ? settings.topBarGothCornersEnabled : false
|
||||||
|
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true
|
||||||
|
hideBrightnessSlider = settings.hideBrightnessSlider !== undefined ? settings.hideBrightnessSlider : false
|
||||||
|
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
|
||||||
|
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "sc"
|
||||||
|
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
|
||||||
applyStoredTheme()
|
applyStoredTheme()
|
||||||
detectAvailableIconThemes()
|
detectAvailableIconThemes()
|
||||||
detectQtTools()
|
detectQtTools()
|
||||||
@@ -285,13 +337,18 @@ Singleton {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
applyStoredTheme()
|
applyStoredTheme()
|
||||||
|
} finally {
|
||||||
|
_loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
|
if (_loading)
|
||||||
|
return
|
||||||
settingsFile.setText(JSON.stringify({
|
settingsFile.setText(JSON.stringify({
|
||||||
"currentThemeName": currentThemeName,
|
"currentThemeName": currentThemeName,
|
||||||
"customThemeFile": customThemeFile,
|
"customThemeFile": customThemeFile,
|
||||||
|
"matugenScheme": matugenScheme,
|
||||||
"topBarTransparency": topBarTransparency,
|
"topBarTransparency": topBarTransparency,
|
||||||
"topBarWidgetTransparency": topBarWidgetTransparency,
|
"topBarWidgetTransparency": topBarWidgetTransparency,
|
||||||
"popupTransparency": popupTransparency,
|
"popupTransparency": popupTransparency,
|
||||||
@@ -323,12 +380,18 @@ Singleton {
|
|||||||
"controlCenterShowNetworkIcon": controlCenterShowNetworkIcon,
|
"controlCenterShowNetworkIcon": controlCenterShowNetworkIcon,
|
||||||
"controlCenterShowBluetoothIcon": controlCenterShowBluetoothIcon,
|
"controlCenterShowBluetoothIcon": controlCenterShowBluetoothIcon,
|
||||||
"controlCenterShowAudioIcon": controlCenterShowAudioIcon,
|
"controlCenterShowAudioIcon": controlCenterShowAudioIcon,
|
||||||
|
"controlCenterWidgets": controlCenterWidgets,
|
||||||
"showWorkspaceIndex": showWorkspaceIndex,
|
"showWorkspaceIndex": showWorkspaceIndex,
|
||||||
"showWorkspacePadding": showWorkspacePadding,
|
"showWorkspacePadding": showWorkspacePadding,
|
||||||
|
"showWorkspaceApps": showWorkspaceApps,
|
||||||
|
"maxWorkspaceIcons": maxWorkspaceIcons,
|
||||||
|
"workspacesPerMonitor": workspacesPerMonitor,
|
||||||
"workspaceNameIcons": workspaceNameIcons,
|
"workspaceNameIcons": workspaceNameIcons,
|
||||||
|
"waveProgressEnabled": waveProgressEnabled,
|
||||||
"clockCompactMode": clockCompactMode,
|
"clockCompactMode": clockCompactMode,
|
||||||
"focusedWindowCompactMode": focusedWindowCompactMode,
|
"focusedWindowCompactMode": focusedWindowCompactMode,
|
||||||
"runningAppsCompactMode": runningAppsCompactMode,
|
"runningAppsCompactMode": runningAppsCompactMode,
|
||||||
|
"runningAppsCurrentWorkspace": runningAppsCurrentWorkspace,
|
||||||
"clockDateFormat": clockDateFormat,
|
"clockDateFormat": clockDateFormat,
|
||||||
"lockDateFormat": lockDateFormat,
|
"lockDateFormat": lockDateFormat,
|
||||||
"mediaSize": mediaSize,
|
"mediaSize": mediaSize,
|
||||||
@@ -343,22 +406,40 @@ Singleton {
|
|||||||
"osLogoColorOverride": osLogoColorOverride,
|
"osLogoColorOverride": osLogoColorOverride,
|
||||||
"osLogoBrightness": osLogoBrightness,
|
"osLogoBrightness": osLogoBrightness,
|
||||||
"osLogoContrast": osLogoContrast,
|
"osLogoContrast": osLogoContrast,
|
||||||
"wallpaperDynamicTheming": wallpaperDynamicTheming,
|
|
||||||
"fontFamily": fontFamily,
|
"fontFamily": fontFamily,
|
||||||
"monoFontFamily": monoFontFamily,
|
"monoFontFamily": monoFontFamily,
|
||||||
"fontWeight": fontWeight,
|
"fontWeight": fontWeight,
|
||||||
|
"fontScale": fontScale,
|
||||||
|
"notepadUseMonospace": notepadUseMonospace,
|
||||||
|
"notepadFontFamily": notepadFontFamily,
|
||||||
|
"notepadFontSize": notepadFontSize,
|
||||||
|
"notepadShowLineNumbers": notepadShowLineNumbers,
|
||||||
|
"notepadTransparencyOverride": notepadTransparencyOverride,
|
||||||
|
"notepadLastCustomTransparency": notepadLastCustomTransparency,
|
||||||
"gtkThemingEnabled": gtkThemingEnabled,
|
"gtkThemingEnabled": gtkThemingEnabled,
|
||||||
"qtThemingEnabled": qtThemingEnabled,
|
"qtThemingEnabled": qtThemingEnabled,
|
||||||
"showDock": showDock,
|
"showDock": showDock,
|
||||||
"dockAutoHide": dockAutoHide,
|
"dockAutoHide": dockAutoHide,
|
||||||
|
"dockGroupByApp": dockGroupByApp,
|
||||||
"cornerRadius": cornerRadius,
|
"cornerRadius": cornerRadius,
|
||||||
"notificationOverlayEnabled": notificationOverlayEnabled,
|
"notificationOverlayEnabled": notificationOverlayEnabled,
|
||||||
"topBarAutoHide": topBarAutoHide,
|
"topBarAutoHide": topBarAutoHide,
|
||||||
|
"topBarOpenOnOverview": topBarOpenOnOverview,
|
||||||
|
"topBarVisible": topBarVisible,
|
||||||
"topBarSpacing": topBarSpacing,
|
"topBarSpacing": topBarSpacing,
|
||||||
|
"topBarBottomGap": topBarBottomGap,
|
||||||
|
"topBarInnerPadding": topBarInnerPadding,
|
||||||
"topBarSquareCorners": topBarSquareCorners,
|
"topBarSquareCorners": topBarSquareCorners,
|
||||||
|
"topBarNoBackground": topBarNoBackground,
|
||||||
|
"topBarGothCornersEnabled": topBarGothCornersEnabled,
|
||||||
|
"lockScreenShowPowerActions": lockScreenShowPowerActions,
|
||||||
|
"hideBrightnessSlider": hideBrightnessSlider,
|
||||||
|
"widgetBackgroundColor": widgetBackgroundColor,
|
||||||
|
"surfaceBase": surfaceBase,
|
||||||
"notificationTimeoutLow": notificationTimeoutLow,
|
"notificationTimeoutLow": notificationTimeoutLow,
|
||||||
"notificationTimeoutNormal": notificationTimeoutNormal,
|
"notificationTimeoutNormal": notificationTimeoutNormal,
|
||||||
"notificationTimeoutCritical": notificationTimeoutCritical
|
"notificationTimeoutCritical": notificationTimeoutCritical,
|
||||||
|
"screenPreferences": screenPreferences
|
||||||
}, null, 2))
|
}, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +453,26 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setShowWorkspaceApps(enabled) {
|
||||||
|
showWorkspaceApps = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMaxWorkspaceIcons(maxIcons) {
|
||||||
|
maxWorkspaceIcons = maxIcons
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWorkspacesPerMonitor(enabled) {
|
||||||
|
workspacesPerMonitor = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWaveProgressEnabled(enabled) {
|
||||||
|
waveProgressEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
function setWorkspaceNameIcon(workspaceName, iconData) {
|
function setWorkspaceNameIcon(workspaceName, iconData) {
|
||||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
||||||
iconMap[workspaceName] = iconData
|
iconMap[workspaceName] = iconData
|
||||||
@@ -409,8 +510,7 @@ Singleton {
|
|||||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
|
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
|
||||||
return namedWorkspaces
|
return namedWorkspaces
|
||||||
|
|
||||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
for (const ws of NiriService.allWorkspaces) {
|
||||||
var ws = NiriService.allWorkspaces[i]
|
|
||||||
if (ws.name && ws.name.trim() !== "") {
|
if (ws.name && ws.name.trim() !== "") {
|
||||||
namedWorkspaces.push(ws.name)
|
namedWorkspaces.push(ws.name)
|
||||||
}
|
}
|
||||||
@@ -433,13 +533,18 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setRunningAppsCurrentWorkspace(enabled) {
|
||||||
|
runningAppsCurrentWorkspace = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
function setClockDateFormat(format) {
|
function setClockDateFormat(format) {
|
||||||
clockDateFormat = format
|
clockDateFormat = format || ""
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLockDateFormat(format) {
|
function setLockDateFormat(format) {
|
||||||
lockDateFormat = format
|
lockDateFormat = format || ""
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +559,7 @@ Singleton {
|
|||||||
else
|
else
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
if (typeof Theme !== "undefined")
|
if (typeof Theme !== "undefined")
|
||||||
Theme.switchTheme(currentThemeName, false)
|
Theme.switchTheme(currentThemeName, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,6 +573,22 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMatugenScheme(scheme) {
|
||||||
|
var normalized = scheme || "scheme-tonal-spot"
|
||||||
|
if (matugenScheme === normalized)
|
||||||
|
return
|
||||||
|
|
||||||
|
matugenScheme = normalized
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (Theme.currentTheme === Theme.dynamic) {
|
||||||
|
Theme.extractColors()
|
||||||
|
}
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setTopBarTransparency(transparency) {
|
function setTopBarTransparency(transparency) {
|
||||||
topBarTransparency = transparency
|
topBarTransparency = transparency
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -604,6 +725,10 @@ Singleton {
|
|||||||
controlCenterShowAudioIcon = enabled
|
controlCenterShowAudioIcon = enabled
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
function setControlCenterWidgets(widgets) {
|
||||||
|
controlCenterWidgets = widgets
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
function setTopBarWidgetOrder(order) {
|
function setTopBarWidgetOrder(order) {
|
||||||
topBarWidgetOrder = order
|
topBarWidgetOrder = order
|
||||||
@@ -634,21 +759,21 @@ Singleton {
|
|||||||
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id
|
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id
|
||||||
var enabled = typeof order[i] === "string" ? true : order[i].enabled
|
var enabled = typeof order[i] === "string" ? true : order[i].enabled
|
||||||
var size = typeof order[i] === "string" ? undefined : order[i].size
|
var size = typeof order[i] === "string" ? undefined : order[i].size
|
||||||
var selectedGpuIndex = typeof order[i]
|
var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex
|
||||||
=== "string" ? undefined : order[i].selectedGpuIndex
|
|
||||||
var pciId = typeof order[i] === "string" ? undefined : order[i].pciId
|
var pciId = typeof order[i] === "string" ? undefined : order[i].pciId
|
||||||
|
var mountPath = typeof order[i] === "string" ? undefined : order[i].mountPath
|
||||||
var item = {
|
var item = {
|
||||||
"widgetId": widgetId,
|
"widgetId": widgetId,
|
||||||
"enabled": enabled
|
"enabled": enabled
|
||||||
}
|
}
|
||||||
if (size !== undefined)
|
if (size !== undefined)
|
||||||
item.size = size
|
item.size = size
|
||||||
|
|
||||||
if (selectedGpuIndex !== undefined)
|
if (selectedGpuIndex !== undefined)
|
||||||
item.selectedGpuIndex = selectedGpuIndex
|
item.selectedGpuIndex = selectedGpuIndex
|
||||||
|
|
||||||
if (pciId !== undefined)
|
if (pciId !== undefined)
|
||||||
item.pciId = pciId
|
item.pciId = pciId
|
||||||
|
if (mountPath !== undefined)
|
||||||
|
item.mountPath = mountPath
|
||||||
|
|
||||||
listModel.append(item)
|
listModel.append(item)
|
||||||
}
|
}
|
||||||
@@ -753,19 +878,16 @@ Singleton {
|
|||||||
|
|
||||||
function updateQtIconTheme(themeName) {
|
function updateQtIconTheme(themeName) {
|
||||||
var qtThemeName = (themeName === "System Default") ? "" : themeName
|
var qtThemeName = (themeName === "System Default") ? "" : themeName
|
||||||
var home = _shq(root._homeUrl.replace("file://", ""))
|
var home = _shq(Paths.strip(root._homeUrl))
|
||||||
if (!qtThemeName) {
|
if (!qtThemeName) {
|
||||||
// When "System Default" is selected, don't modify the config files at all
|
// When "System Default" is selected, don't modify the config files at all
|
||||||
// This preserves the user's existing qt6ct configuration
|
// This preserves the user's existing qt6ct configuration
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var script = "mkdir -p " + _configDir + "/qt5ct " + _configDir + "/qt6ct " + _configDir + "/environment.d 2>/dev/null || true\n" + "update_qt_config() {\n" + " local config_file=\"$1\"\n"
|
var script = "mkdir -p " + _configDir + "/qt5ct " + _configDir + "/qt6ct " + _configDir + "/environment.d 2>/dev/null || true\n" + "update_qt_icon_theme() {\n" + " local config_file=\"$1\"\n"
|
||||||
+ " local theme_name=\"$2\"\n" + " if [ -f \"$config_file\" ]; then\n" + " if grep -q '^\\[Appearance\\]' \"$config_file\"; then\n" + " awk -v theme=\"$theme_name\" '\n" + " BEGIN { in_appearance = 0; icon_theme_added = 0 }\n" + " /^\\[Appearance\\]/ { in_appearance = 1; print; next }\n" + " /^\\[/ && !/^\\[Appearance\\]/ { \n" + " if (in_appearance && !icon_theme_added) { \n"
|
+ " local theme_name=\"$2\"\n" + " if [ -f \"$config_file\" ]; then\n" + " if grep -q '^\\[Appearance\\]' \"$config_file\"; then\n" + " if grep -q '^icon_theme=' \"$config_file\"; then\n" + " sed -i \"s/^icon_theme=.*/icon_theme=$theme_name/\" \"$config_file\"\n" + " else\n" + " sed -i \"/^\\[Appearance\\]/a icon_theme=$theme_name\" \"$config_file\"\n" + " fi\n"
|
||||||
+ " print \"icon_theme=\" theme; icon_theme_added = 1 \n" + " } \n" + " in_appearance = 0; print; next \n" + " }\n" + " in_appearance && /^icon_theme=/ { \n" + " if (!icon_theme_added) { \n" + " print \"icon_theme=\" theme; icon_theme_added = 1 \n" + " } \n"
|
+ " else\n" + " printf '\\n[Appearance]\\nicon_theme=%s\\n' \"$theme_name\" >> \"$config_file\"\n" + " fi\n" + " else\n" + " printf '[Appearance]\\nicon_theme=%s\\n' \"$theme_name\" > \"$config_file\"\n" + " fi\n" + "}\n" + "update_qt_icon_theme " + _configDir + "/qt5ct/qt5ct.conf " + _shq(
|
||||||
+ " next \n" + " }\n" + " { print }\n" + " END { if (in_appearance && !icon_theme_added) print \"icon_theme=\" theme }\n" + " ' \"$config_file\" > \"$config_file.tmp\" && mv \"$config_file.tmp\" \"$config_file\"\n" + " else\n" + " printf '\\n[Appearance]\\nicon_theme=%s\\n' \"$theme_name\" >> \"$config_file\"\n" + " fi\n"
|
qtThemeName) + "\n" + "update_qt_icon_theme " + _configDir + "/qt6ct/qt6ct.conf " + _shq(qtThemeName) + "\n" + "rm -rf " + home + "/.cache/icon-cache " + home + "/.cache/thumbnails 2>/dev/null || true\n"
|
||||||
+ " else\n" + " printf '[Appearance]\\nicon_theme=%s\\n' \"$theme_name\" > \"$config_file\"\n" + " fi\n" + "}\n" + "update_qt_config " + _configDir + "/qt5ct/qt5ct.conf " + _shq(
|
|
||||||
qtThemeName) + "\n" + "update_qt_config " + _configDir + "/qt6ct/qt6ct.conf " + _shq(qtThemeName) + "\n"
|
|
||||||
+ "rm -rf " + home + "/.cache/icon-cache " + home + "/.cache/thumbnails 2>/dev/null || true\n"
|
|
||||||
Quickshell.execDetached(["sh", "-lc", script])
|
Quickshell.execDetached(["sh", "-lc", script])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,11 +916,6 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaperDynamicTheming(enabled) {
|
|
||||||
wallpaperDynamicTheming = enabled
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFontFamily(family) {
|
function setFontFamily(family) {
|
||||||
fontFamily = family
|
fontFamily = family
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -814,6 +931,11 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFontScale(scale) {
|
||||||
|
fontScale = scale
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
function setGtkThemingEnabled(enabled) {
|
function setGtkThemingEnabled(enabled) {
|
||||||
gtkThemingEnabled = enabled
|
gtkThemingEnabled = enabled
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -840,6 +962,11 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDockGroupByApp(enabled) {
|
||||||
|
dockGroupByApp = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
function setCornerRadius(radius) {
|
function setCornerRadius(radius) {
|
||||||
cornerRadius = radius
|
cornerRadius = radius
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -855,6 +982,21 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTopBarOpenOnOverview(enabled) {
|
||||||
|
topBarOpenOnOverview = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTopBarVisible(visible) {
|
||||||
|
topBarVisible = visible
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTopBarVisible() {
|
||||||
|
topBarVisible = !topBarVisible
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
function setNotificationTimeoutLow(timeout) {
|
function setNotificationTimeoutLow(timeout) {
|
||||||
notificationTimeoutLow = timeout
|
notificationTimeoutLow = timeout
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -875,11 +1017,67 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTopBarBottomGap(gap) {
|
||||||
|
topBarBottomGap = gap
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTopBarInnerPadding(padding) {
|
||||||
|
topBarInnerPadding = padding
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
function setTopBarSquareCorners(enabled) {
|
function setTopBarSquareCorners(enabled) {
|
||||||
topBarSquareCorners = enabled
|
topBarSquareCorners = enabled
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTopBarNoBackground(enabled) {
|
||||||
|
topBarNoBackground = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTopBarGothCornersEnabled(enabled) {
|
||||||
|
topBarGothCornersEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLockScreenShowPowerActions(enabled) {
|
||||||
|
lockScreenShowPowerActions = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHideBrightnessSlider(enabled) {
|
||||||
|
hideBrightnessSlider = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWidgetBackgroundColor(color) {
|
||||||
|
widgetBackgroundColor = color
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSurfaceBase(base) {
|
||||||
|
surfaceBase = base
|
||||||
|
saveSettings()
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setScreenPreferences(prefs) {
|
||||||
|
screenPreferences = prefs
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredScreens(componentId) {
|
||||||
|
var prefs = screenPreferences && screenPreferences[componentId] || ["all"]
|
||||||
|
if (prefs.includes("all")) {
|
||||||
|
return Quickshell.screens
|
||||||
|
}
|
||||||
|
return Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
||||||
|
}
|
||||||
|
|
||||||
function _shq(s) {
|
function _shq(s) {
|
||||||
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||||
}
|
}
|
||||||
@@ -910,36 +1108,41 @@ Singleton {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
var availableFonts = Qt.fontFamilies()
|
var availableFonts = Qt.fontFamilies()
|
||||||
var missingFonts = []
|
var missingFonts = []
|
||||||
if (fontFamily === defaultFontFamily && !availableFonts.includes(
|
if (fontFamily === defaultFontFamily && !availableFonts.includes(defaultFontFamily))
|
||||||
defaultFontFamily))
|
missingFonts.push(defaultFontFamily)
|
||||||
missingFonts.push(defaultFontFamily)
|
|
||||||
|
|
||||||
if (monoFontFamily === defaultMonoFontFamily
|
if (monoFontFamily === defaultMonoFontFamily && !availableFonts.includes(defaultMonoFontFamily))
|
||||||
&& !availableFonts.includes(defaultMonoFontFamily))
|
missingFonts.push(defaultMonoFontFamily)
|
||||||
missingFonts.push(defaultMonoFontFamily)
|
|
||||||
|
|
||||||
if (missingFonts.length > 0) {
|
if (missingFonts.length > 0) {
|
||||||
var message = "Missing fonts: " + missingFonts.join(
|
var message = "Missing fonts: " + missingFonts.join(", ") + ". Using system defaults."
|
||||||
", ") + ". Using system defaults."
|
|
||||||
ToastService.showWarning(message)
|
ToastService.showWarning(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool hasTriedDefaultSettings: false
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: settingsFile
|
id: settingsFile
|
||||||
|
|
||||||
path: StandardPaths.writableLocation(
|
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||||
StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
|
atomicWrites: true
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
parseSettings(settingsFile.text())
|
parseSettings(settingsFile.text())
|
||||||
|
hasTriedDefaultSettings = false
|
||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
applyStoredTheme()
|
if (!hasTriedDefaultSettings) {
|
||||||
}
|
hasTriedDefaultSettings = true
|
||||||
|
defaultSettingsCheckProcess.running = true
|
||||||
|
} else {
|
||||||
|
applyStoredTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
@@ -948,12 +1151,12 @@ Singleton {
|
|||||||
command: ["sh", "-c", "gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed \"s/'//g\" || echo ''"]
|
command: ["sh", "-c", "gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed \"s/'//g\" || echo ''"]
|
||||||
running: false
|
running: false
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
if (exitCode === 0 && stdout && stdout.length > 0)
|
if (exitCode === 0 && stdout && stdout.length > 0)
|
||||||
systemDefaultIconTheme = stdout.trim()
|
systemDefaultIconTheme = stdout.trim()
|
||||||
else
|
else
|
||||||
systemDefaultIconTheme = ""
|
systemDefaultIconTheme = ""
|
||||||
iconThemeDetectionProcess.running = true
|
iconThemeDetectionProcess.running = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
@@ -969,9 +1172,8 @@ Singleton {
|
|||||||
var themes = text.trim().split('\n')
|
var themes = text.trim().split('\n')
|
||||||
for (var i = 0; i < themes.length; i++) {
|
for (var i = 0; i < themes.length; i++) {
|
||||||
var theme = themes[i].trim()
|
var theme = themes[i].trim()
|
||||||
if (theme && theme !== "" && theme !== "default"
|
if (theme && theme !== "" && theme !== "default" && theme !== "hicolor" && theme !== "locolor")
|
||||||
&& theme !== "hicolor" && theme !== "locolor")
|
detectedThemes.push(theme)
|
||||||
detectedThemes.push(theme)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
availableIconThemes = detectedThemes
|
availableIconThemes = detectedThemes
|
||||||
@@ -992,14 +1194,54 @@ Singleton {
|
|||||||
for (var i = 0; i < lines.length; i++) {
|
for (var i = 0; i < lines.length; i++) {
|
||||||
var line = lines[i]
|
var line = lines[i]
|
||||||
if (line.startsWith('qt5ct:'))
|
if (line.startsWith('qt5ct:'))
|
||||||
qt5ctAvailable = line.split(':')[1] === 'true'
|
qt5ctAvailable = line.split(':')[1] === 'true'
|
||||||
else if (line.startsWith('qt6ct:'))
|
else if (line.startsWith('qt6ct:'))
|
||||||
qt6ctAvailable = line.split(':')[1] === 'true'
|
qt6ctAvailable = line.split(':')[1] === 'true'
|
||||||
else if (line.startsWith('gtk:'))
|
else if (line.startsWith('gtk:'))
|
||||||
gtkAvailable = line.split(':')[1] === 'true'
|
gtkAvailable = line.split(':')[1] === 'true'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: defaultSettingsCheckProcess
|
||||||
|
|
||||||
|
command: ["sh", "-c", "CONFIG_DIR=\"" + _configDir
|
||||||
|
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-settings.json\" ] && [ ! -f \"$CONFIG_DIR/settings.json\" ]; then cp \"$CONFIG_DIR/default-settings.json\" \"$CONFIG_DIR/settings.json\" && echo 'copied'; else echo 'not_found'; fi"]
|
||||||
|
running: false
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
console.log("Copied default-settings.json to settings.json")
|
||||||
|
settingsFile.reload()
|
||||||
|
} else {
|
||||||
|
// No default settings file found, just apply stored theme
|
||||||
|
applyStoredTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function reveal(): string {
|
||||||
|
root.setTopBarVisible(true)
|
||||||
|
return "BAR_SHOW_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(): string {
|
||||||
|
root.setTopBarVisible(false)
|
||||||
|
return "BAR_HIDE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
root.toggleTopBarVisible()
|
||||||
|
return topBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
return topBarVisible ? "visible" : "hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "bar"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,124 @@
|
|||||||
// Stock theme definitions for DankMaterialShell
|
// Stock theme definitions for DankMaterialShell
|
||||||
// Separated from Theme.qml to keep that file clean
|
// Separated from Theme.qml to keep that file clean
|
||||||
|
|
||||||
|
const CatppuccinMocha = {
|
||||||
|
surface: "#45475a",
|
||||||
|
surfaceText: "#cdd6f4",
|
||||||
|
surfaceVariant: "#45475a",
|
||||||
|
surfaceVariantText: "#a6adc8",
|
||||||
|
background: "#1e1e2e",
|
||||||
|
backgroundText: "#cdd6f4",
|
||||||
|
outline: "#6c7086",
|
||||||
|
surfaceContainer: "#313244",
|
||||||
|
surfaceContainerHigh: "#585b70",
|
||||||
|
surfaceContainerHighest: "#7f849c"
|
||||||
|
}
|
||||||
|
|
||||||
|
const CatppuccinLatte = {
|
||||||
|
surface: "#bcc0cc",
|
||||||
|
surfaceText: "#4c4f69",
|
||||||
|
surfaceVariant: "#bcc0cc",
|
||||||
|
surfaceVariantText: "#6c6f85",
|
||||||
|
background: "#eff1f5",
|
||||||
|
backgroundText: "#4c4f69",
|
||||||
|
outline: "#9ca0b0",
|
||||||
|
surfaceContainer: "#ccd0da",
|
||||||
|
surfaceContainerHigh: "#acb0be",
|
||||||
|
surfaceContainerHighest: "#8c8fa1"
|
||||||
|
}
|
||||||
|
|
||||||
|
const CatppuccinVariants = {
|
||||||
|
"cat-rosewater": {
|
||||||
|
name: "Rosewater",
|
||||||
|
dark: { primary: "#f5e0dc", secondary: "#f2cdcd", primaryText: "#1e1e2e", primaryContainer: "#8b6b5e", surfaceTint: "#f5e0dc" },
|
||||||
|
light: { primary: "#dc8a78", secondary: "#dd7878", primaryText: "#ffffff", primaryContainer: "#f4d2ca", surfaceTint: "#dc8a78" }
|
||||||
|
},
|
||||||
|
"cat-flamingo": {
|
||||||
|
name: "Flamingo",
|
||||||
|
dark: { primary: "#f2cdcd", secondary: "#f5e0dc", primaryText: "#1e1e2e", primaryContainer: "#885d62", surfaceTint: "#f2cdcd" },
|
||||||
|
light: { primary: "#dd7878", secondary: "#dc8a78", primaryText: "#ffffff", primaryContainer: "#f4caca", surfaceTint: "#dd7878" }
|
||||||
|
},
|
||||||
|
"cat-pink": {
|
||||||
|
name: "Pink",
|
||||||
|
dark: { primary: "#f5c2e7", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#8b537a", surfaceTint: "#f5c2e7" },
|
||||||
|
light: { primary: "#ea76cb", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#f7c9e7", surfaceTint: "#ea76cb" }
|
||||||
|
},
|
||||||
|
"cat-mauve": {
|
||||||
|
name: "Mauve",
|
||||||
|
dark: { primary: "#cba6f7", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#61378a", surfaceTint: "#cba6f7" },
|
||||||
|
light: { primary: "#8839ef", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#e4d3ff", surfaceTint: "#8839ef" }
|
||||||
|
},
|
||||||
|
"cat-red": {
|
||||||
|
name: "Red",
|
||||||
|
dark: { primary: "#f38ba8", secondary: "#eba0ac", primaryText: "#1e1e2e", primaryContainer: "#891c3b", surfaceTint: "#f38ba8" },
|
||||||
|
light: { primary: "#d20f39", secondary: "#e64553", primaryText: "#ffffff", primaryContainer: "#f1b8c4", surfaceTint: "#d20f39" }
|
||||||
|
},
|
||||||
|
"cat-maroon": {
|
||||||
|
name: "Maroon",
|
||||||
|
dark: { primary: "#eba0ac", secondary: "#f38ba8", primaryText: "#1e1e2e", primaryContainer: "#81313f", surfaceTint: "#eba0ac" },
|
||||||
|
light: { primary: "#e64553", secondary: "#d20f39", primaryText: "#ffffff", primaryContainer: "#f4c3c8", surfaceTint: "#e64553" }
|
||||||
|
},
|
||||||
|
"cat-peach": {
|
||||||
|
name: "Peach",
|
||||||
|
dark: { primary: "#fab387", secondary: "#f9e2af", primaryText: "#1e1e2e", primaryContainer: "#90441a", surfaceTint: "#fab387" },
|
||||||
|
light: { primary: "#fe640b", secondary: "#df8e1d", primaryText: "#ffffff", primaryContainer: "#ffddcc", surfaceTint: "#fe640b" }
|
||||||
|
},
|
||||||
|
"cat-yellow": {
|
||||||
|
name: "Yellow",
|
||||||
|
dark: { primary: "#f9e2af", secondary: "#a6e3a1", primaryText: "#1e1e2e", primaryContainer: "#8f7342", surfaceTint: "#f9e2af" },
|
||||||
|
light: { primary: "#df8e1d", secondary: "#40a02b", primaryText: "#ffffff", primaryContainer: "#fff3cc", surfaceTint: "#df8e1d" }
|
||||||
|
},
|
||||||
|
"cat-green": {
|
||||||
|
name: "Green",
|
||||||
|
dark: { primary: "#a6e3a1", secondary: "#94e2d5", primaryText: "#1e1e2e", primaryContainer: "#3c7534", surfaceTint: "#a6e3a1" },
|
||||||
|
light: { primary: "#40a02b", secondary: "#179299", primaryText: "#ffffff", primaryContainer: "#d4f5d4", surfaceTint: "#40a02b" }
|
||||||
|
},
|
||||||
|
"cat-teal": {
|
||||||
|
name: "Teal",
|
||||||
|
dark: { primary: "#94e2d5", secondary: "#89dceb", primaryText: "#1e1e2e", primaryContainer: "#2a7468", surfaceTint: "#94e2d5" },
|
||||||
|
light: { primary: "#179299", secondary: "#04a5e5", primaryText: "#ffffff", primaryContainer: "#ccf2f2", surfaceTint: "#179299" }
|
||||||
|
},
|
||||||
|
"cat-sky": {
|
||||||
|
name: "Sky",
|
||||||
|
dark: { primary: "#89dceb", secondary: "#74c7ec", primaryText: "#1e1e2e", primaryContainer: "#196e7e", surfaceTint: "#89dceb" },
|
||||||
|
light: { primary: "#04a5e5", secondary: "#209fb5", primaryText: "#ffffff", primaryContainer: "#ccebff", surfaceTint: "#04a5e5" }
|
||||||
|
},
|
||||||
|
"cat-sapphire": {
|
||||||
|
name: "Sapphire",
|
||||||
|
dark: { primary: "#74c7ec", secondary: "#89b4fa", primaryText: "#1e1e2e", primaryContainer: "#0a597f", surfaceTint: "#74c7ec" },
|
||||||
|
light: { primary: "#209fb5", secondary: "#1e66f5", primaryText: "#ffffff", primaryContainer: "#d0f0f5", surfaceTint: "#209fb5" }
|
||||||
|
},
|
||||||
|
"cat-blue": {
|
||||||
|
name: "Blue",
|
||||||
|
dark: { primary: "#89b4fa", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#19468d", surfaceTint: "#89b4fa" },
|
||||||
|
light: { primary: "#1e66f5", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#ccd9ff", surfaceTint: "#1e66f5" }
|
||||||
|
},
|
||||||
|
"cat-lavender": {
|
||||||
|
name: "Lavender",
|
||||||
|
dark: { primary: "#b4befe", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#4a5091", surfaceTint: "#b4befe" },
|
||||||
|
light: { primary: "#7287fd", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#dde1ff", surfaceTint: "#7287fd" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCatppuccinTheme(variant, isLight = false) {
|
||||||
|
const variantData = CatppuccinVariants[variant]
|
||||||
|
if (!variantData) return null
|
||||||
|
|
||||||
|
const baseColors = isLight ? CatppuccinLatte : CatppuccinMocha
|
||||||
|
const accentColors = isLight ? variantData.light : variantData.dark
|
||||||
|
|
||||||
|
return Object.assign({
|
||||||
|
name: `${variantData.name}${isLight ? ' Light' : ''}`
|
||||||
|
}, baseColors, accentColors)
|
||||||
|
}
|
||||||
|
|
||||||
const StockThemes = {
|
const StockThemes = {
|
||||||
DARK: {
|
DARK: {
|
||||||
blue: {
|
blue: {
|
||||||
name: "Blue",
|
name: "Blue",
|
||||||
primary: "#42a5f5",
|
primary: "#42a5f5",
|
||||||
primaryText: "#ffffff",
|
primaryText: "#000000",
|
||||||
primaryContainer: "#1976d2",
|
primaryContainer: "#0d47a1",
|
||||||
secondary: "#8ab4f8",
|
secondary: "#8ab4f8",
|
||||||
surface: "#1a1c1e",
|
surface: "#1a1c1e",
|
||||||
surfaceText: "#e3e8ef",
|
surfaceText: "#e3e8ef",
|
||||||
@@ -18,24 +129,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#e3e8ef",
|
backgroundText: "#e3e8ef",
|
||||||
outline: "#8e918f",
|
outline: "#8e918f",
|
||||||
surfaceContainer: "#1e2023",
|
surfaceContainer: "#1e2023",
|
||||||
surfaceContainerHigh: "#292b2f"
|
surfaceContainerHigh: "#292b2f",
|
||||||
},
|
surfaceContainerHighest: "#343740"
|
||||||
deepBlue: {
|
|
||||||
name: "Deep Blue",
|
|
||||||
primary: "#0061a4",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#004881",
|
|
||||||
secondary: "#42a5f5",
|
|
||||||
surface: "#1a1c1e",
|
|
||||||
surfaceText: "#e3e8ef",
|
|
||||||
surfaceVariant: "#44464f",
|
|
||||||
surfaceVariantText: "#c4c7c5",
|
|
||||||
surfaceTint: "#8ab4f8",
|
|
||||||
background: "#1a1c1e",
|
|
||||||
backgroundText: "#e3e8ef",
|
|
||||||
outline: "#8e918f",
|
|
||||||
surfaceContainer: "#1e2023",
|
|
||||||
surfaceContainerHigh: "#292b2f"
|
|
||||||
},
|
},
|
||||||
purple: {
|
purple: {
|
||||||
name: "Purple",
|
name: "Purple",
|
||||||
@@ -52,13 +147,14 @@ const StockThemes = {
|
|||||||
backgroundText: "#E6E0E9",
|
backgroundText: "#E6E0E9",
|
||||||
outline: "#938F99",
|
outline: "#938F99",
|
||||||
surfaceContainer: "#1D1B20",
|
surfaceContainer: "#1D1B20",
|
||||||
surfaceContainerHigh: "#2B2930"
|
surfaceContainerHigh: "#2B2930",
|
||||||
|
surfaceContainerHighest: "#36343B"
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
name: "Green",
|
name: "Green",
|
||||||
primary: "#4caf50",
|
primary: "#4caf50",
|
||||||
primaryText: "#ffffff",
|
primaryText: "#000000",
|
||||||
primaryContainer: "#388e3c",
|
primaryContainer: "#1b5e20",
|
||||||
secondary: "#81c995",
|
secondary: "#81c995",
|
||||||
surface: "#0f1411",
|
surface: "#0f1411",
|
||||||
surfaceText: "#e1f5e3",
|
surfaceText: "#e1f5e3",
|
||||||
@@ -69,13 +165,14 @@ const StockThemes = {
|
|||||||
backgroundText: "#e1f5e3",
|
backgroundText: "#e1f5e3",
|
||||||
outline: "#8b938c",
|
outline: "#8b938c",
|
||||||
surfaceContainer: "#1a1f1b",
|
surfaceContainer: "#1a1f1b",
|
||||||
surfaceContainerHigh: "#252a26"
|
surfaceContainerHigh: "#252a26",
|
||||||
|
surfaceContainerHighest: "#30352f"
|
||||||
},
|
},
|
||||||
orange: {
|
orange: {
|
||||||
name: "Orange",
|
name: "Orange",
|
||||||
primary: "#ff6d00",
|
primary: "#ff6d00",
|
||||||
primaryText: "#ffffff",
|
primaryText: "#000000",
|
||||||
primaryContainer: "#e65100",
|
primaryContainer: "#3e2723",
|
||||||
secondary: "#ffb74d",
|
secondary: "#ffb74d",
|
||||||
surface: "#1c1410",
|
surface: "#1c1410",
|
||||||
surfaceText: "#f5f1ea",
|
surfaceText: "#f5f1ea",
|
||||||
@@ -86,13 +183,14 @@ const StockThemes = {
|
|||||||
backgroundText: "#f5f1ea",
|
backgroundText: "#f5f1ea",
|
||||||
outline: "#958f84",
|
outline: "#958f84",
|
||||||
surfaceContainer: "#211e17",
|
surfaceContainer: "#211e17",
|
||||||
surfaceContainerHigh: "#2c291f"
|
surfaceContainerHigh: "#2c291f",
|
||||||
|
surfaceContainerHighest: "#373427"
|
||||||
},
|
},
|
||||||
red: {
|
red: {
|
||||||
name: "Red",
|
name: "Red",
|
||||||
primary: "#f44336",
|
primary: "#f44336",
|
||||||
primaryText: "#ffffff",
|
primaryText: "#000000",
|
||||||
primaryContainer: "#d32f2f",
|
primaryContainer: "#4a0e0e",
|
||||||
secondary: "#f28b82",
|
secondary: "#f28b82",
|
||||||
surface: "#1c1011",
|
surface: "#1c1011",
|
||||||
surfaceText: "#f5e8ea",
|
surfaceText: "#f5e8ea",
|
||||||
@@ -103,13 +201,14 @@ const StockThemes = {
|
|||||||
backgroundText: "#f5e8ea",
|
backgroundText: "#f5e8ea",
|
||||||
outline: "#958b8d",
|
outline: "#958b8d",
|
||||||
surfaceContainer: "#211b1c",
|
surfaceContainer: "#211b1c",
|
||||||
surfaceContainerHigh: "#2c2426"
|
surfaceContainerHigh: "#2c2426",
|
||||||
|
surfaceContainerHighest: "#372f30"
|
||||||
},
|
},
|
||||||
cyan: {
|
cyan: {
|
||||||
name: "Cyan",
|
name: "Cyan",
|
||||||
primary: "#00bcd4",
|
primary: "#00bcd4",
|
||||||
primaryText: "#ffffff",
|
primaryText: "#000000",
|
||||||
primaryContainer: "#0097a7",
|
primaryContainer: "#004d5c",
|
||||||
secondary: "#4dd0e1",
|
secondary: "#4dd0e1",
|
||||||
surface: "#0f1617",
|
surface: "#0f1617",
|
||||||
surfaceText: "#e8f4f5",
|
surfaceText: "#e8f4f5",
|
||||||
@@ -120,13 +219,14 @@ const StockThemes = {
|
|||||||
backgroundText: "#e8f4f5",
|
backgroundText: "#e8f4f5",
|
||||||
outline: "#8c9194",
|
outline: "#8c9194",
|
||||||
surfaceContainer: "#1a1f20",
|
surfaceContainer: "#1a1f20",
|
||||||
surfaceContainerHigh: "#252b2c"
|
surfaceContainerHigh: "#252b2c",
|
||||||
|
surfaceContainerHighest: "#303637"
|
||||||
},
|
},
|
||||||
pink: {
|
pink: {
|
||||||
name: "Pink",
|
name: "Pink",
|
||||||
primary: "#e91e63",
|
primary: "#e91e63",
|
||||||
primaryText: "#ffffff",
|
primaryText: "#000000",
|
||||||
primaryContainer: "#c2185b",
|
primaryContainer: "#4a0e2f",
|
||||||
secondary: "#f8bbd9",
|
secondary: "#f8bbd9",
|
||||||
surface: "#1a1014",
|
surface: "#1a1014",
|
||||||
surfaceText: "#f3e8ee",
|
surfaceText: "#f3e8ee",
|
||||||
@@ -137,13 +237,14 @@ const StockThemes = {
|
|||||||
backgroundText: "#f3e8ee",
|
backgroundText: "#f3e8ee",
|
||||||
outline: "#938a90",
|
outline: "#938a90",
|
||||||
surfaceContainer: "#1f1b1e",
|
surfaceContainer: "#1f1b1e",
|
||||||
surfaceContainerHigh: "#2a2428"
|
surfaceContainerHigh: "#2a2428",
|
||||||
|
surfaceContainerHighest: "#352f32"
|
||||||
},
|
},
|
||||||
amber: {
|
amber: {
|
||||||
name: "Amber",
|
name: "Amber",
|
||||||
primary: "#ffc107",
|
primary: "#ffc107",
|
||||||
primaryText: "#000000",
|
primaryText: "#000000",
|
||||||
primaryContainer: "#ff8f00",
|
primaryContainer: "#4a3c00",
|
||||||
secondary: "#ffd54f",
|
secondary: "#ffd54f",
|
||||||
surface: "#1a1710",
|
surface: "#1a1710",
|
||||||
surfaceText: "#f3f0e8",
|
surfaceText: "#f3f0e8",
|
||||||
@@ -154,12 +255,13 @@ const StockThemes = {
|
|||||||
backgroundText: "#f3f0e8",
|
backgroundText: "#f3f0e8",
|
||||||
outline: "#949084",
|
outline: "#949084",
|
||||||
surfaceContainer: "#1f1e17",
|
surfaceContainer: "#1f1e17",
|
||||||
surfaceContainerHigh: "#2a281f"
|
surfaceContainerHigh: "#2a281f",
|
||||||
|
surfaceContainerHighest: "#353327"
|
||||||
},
|
},
|
||||||
coral: {
|
coral: {
|
||||||
name: "Coral",
|
name: "Coral",
|
||||||
primary: "#ffb4ab",
|
primary: "#ffb4ab",
|
||||||
primaryText: "#5f1412",
|
primaryText: "#000000",
|
||||||
primaryContainer: "#8c1d18",
|
primaryContainer: "#8c1d18",
|
||||||
secondary: "#f9dedc",
|
secondary: "#f9dedc",
|
||||||
surface: "#1a1110",
|
surface: "#1a1110",
|
||||||
@@ -171,7 +273,30 @@ const StockThemes = {
|
|||||||
backgroundText: "#f1e8e7",
|
backgroundText: "#f1e8e7",
|
||||||
outline: "#968b8a",
|
outline: "#968b8a",
|
||||||
surfaceContainer: "#201a19",
|
surfaceContainer: "#201a19",
|
||||||
surfaceContainerHigh: "#2b2221"
|
surfaceContainerHigh: "#2b2221",
|
||||||
|
surfaceContainerHighest: "#362d29"
|
||||||
|
},
|
||||||
|
monochrome: {
|
||||||
|
name: "Monochrome",
|
||||||
|
primary: "#ffffff",
|
||||||
|
primaryText: "#2b303c",
|
||||||
|
primaryContainer: "#424753",
|
||||||
|
secondary: "#c4c6d0",
|
||||||
|
surface: "#2a2a2a",
|
||||||
|
surfaceText: "#e4e2e3",
|
||||||
|
surfaceVariant: "#474648",
|
||||||
|
surfaceVariantText: "#c8c6c7",
|
||||||
|
surfaceTint: "#c2c6d6",
|
||||||
|
background: "#131315",
|
||||||
|
backgroundText: "#e4e2e3",
|
||||||
|
outline: "#929092",
|
||||||
|
surfaceContainer: "#2a2a2a",
|
||||||
|
surfaceContainerHigh: "#2a2a2b",
|
||||||
|
surfaceContainerHighest: "#353535",
|
||||||
|
error: "#ffb4ab",
|
||||||
|
warning: "#3f4759",
|
||||||
|
info: "#595e6c",
|
||||||
|
matugen_type: "scheme-monochrome"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
LIGHT: {
|
LIGHT: {
|
||||||
@@ -190,24 +315,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#1a1c1e",
|
backgroundText: "#1a1c1e",
|
||||||
outline: "#79747e",
|
outline: "#79747e",
|
||||||
surfaceContainer: "#f3f3f3",
|
surfaceContainer: "#f3f3f3",
|
||||||
surfaceContainerHigh: "#ececec"
|
surfaceContainerHigh: "#ececec",
|
||||||
},
|
surfaceContainerHighest: "#e6e6e6"
|
||||||
deepBlue: {
|
|
||||||
name: "Deep Blue Light",
|
|
||||||
primary: "#0061a4",
|
|
||||||
primaryText: "#ffffff",
|
|
||||||
primaryContainer: "#cfe5ff",
|
|
||||||
secondary: "#1976d2",
|
|
||||||
surface: "#fefefe",
|
|
||||||
surfaceText: "#1a1c1e",
|
|
||||||
surfaceVariant: "#e7e0ec",
|
|
||||||
surfaceVariantText: "#49454f",
|
|
||||||
surfaceTint: "#0061a4",
|
|
||||||
background: "#fefefe",
|
|
||||||
backgroundText: "#1a1c1e",
|
|
||||||
outline: "#79747e",
|
|
||||||
surfaceContainer: "#f3f3f3",
|
|
||||||
surfaceContainerHigh: "#ececec"
|
|
||||||
},
|
},
|
||||||
purple: {
|
purple: {
|
||||||
name: "Purple Light",
|
name: "Purple Light",
|
||||||
@@ -224,7 +333,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#1C1B1F",
|
backgroundText: "#1C1B1F",
|
||||||
outline: "#79747E",
|
outline: "#79747E",
|
||||||
surfaceContainer: "#F3EDF7",
|
surfaceContainer: "#F3EDF7",
|
||||||
surfaceContainerHigh: "#ECE6F0"
|
surfaceContainerHigh: "#ECE6F0",
|
||||||
|
surfaceContainerHighest: "#E6DFE9"
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
name: "Green Light",
|
name: "Green Light",
|
||||||
@@ -241,7 +351,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#1a1c1e",
|
backgroundText: "#1a1c1e",
|
||||||
outline: "#79747e",
|
outline: "#79747e",
|
||||||
surfaceContainer: "#f3f3f3",
|
surfaceContainer: "#f3f3f3",
|
||||||
surfaceContainerHigh: "#ececec"
|
surfaceContainerHigh: "#ececec",
|
||||||
|
surfaceContainerHighest: "#e6e6e6"
|
||||||
},
|
},
|
||||||
orange: {
|
orange: {
|
||||||
name: "Orange Light",
|
name: "Orange Light",
|
||||||
@@ -258,7 +369,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#1a1c1e",
|
backgroundText: "#1a1c1e",
|
||||||
outline: "#79747e",
|
outline: "#79747e",
|
||||||
surfaceContainer: "#f3f3f3",
|
surfaceContainer: "#f3f3f3",
|
||||||
surfaceContainerHigh: "#ececec"
|
surfaceContainerHigh: "#ececec",
|
||||||
|
surfaceContainerHighest: "#e6e6e6"
|
||||||
},
|
},
|
||||||
red: {
|
red: {
|
||||||
name: "Red Light",
|
name: "Red Light",
|
||||||
@@ -275,7 +387,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#1a1c1e",
|
backgroundText: "#1a1c1e",
|
||||||
outline: "#79747e",
|
outline: "#79747e",
|
||||||
surfaceContainer: "#f3f3f3",
|
surfaceContainer: "#f3f3f3",
|
||||||
surfaceContainerHigh: "#ececec"
|
surfaceContainerHigh: "#ececec",
|
||||||
|
surfaceContainerHighest: "#e6e6e6"
|
||||||
},
|
},
|
||||||
cyan: {
|
cyan: {
|
||||||
name: "Cyan Light",
|
name: "Cyan Light",
|
||||||
@@ -292,7 +405,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#1a1c1e",
|
backgroundText: "#1a1c1e",
|
||||||
outline: "#79747e",
|
outline: "#79747e",
|
||||||
surfaceContainer: "#f3f3f3",
|
surfaceContainer: "#f3f3f3",
|
||||||
surfaceContainerHigh: "#ececec"
|
surfaceContainerHigh: "#ececec",
|
||||||
|
surfaceContainerHighest: "#e6e6e6"
|
||||||
},
|
},
|
||||||
pink: {
|
pink: {
|
||||||
name: "Pink Light",
|
name: "Pink Light",
|
||||||
@@ -309,7 +423,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#1a1c1e",
|
backgroundText: "#1a1c1e",
|
||||||
outline: "#79747e",
|
outline: "#79747e",
|
||||||
surfaceContainer: "#f3f3f3",
|
surfaceContainer: "#f3f3f3",
|
||||||
surfaceContainerHigh: "#ececec"
|
surfaceContainerHigh: "#ececec",
|
||||||
|
surfaceContainerHighest: "#e6e6e6"
|
||||||
},
|
},
|
||||||
amber: {
|
amber: {
|
||||||
name: "Amber Light",
|
name: "Amber Light",
|
||||||
@@ -326,7 +441,8 @@ const StockThemes = {
|
|||||||
backgroundText: "#1a1c1e",
|
backgroundText: "#1a1c1e",
|
||||||
outline: "#79747e",
|
outline: "#79747e",
|
||||||
surfaceContainer: "#f3f3f3",
|
surfaceContainer: "#f3f3f3",
|
||||||
surfaceContainerHigh: "#ececec"
|
surfaceContainerHigh: "#ececec",
|
||||||
|
surfaceContainerHighest: "#e6e6e6"
|
||||||
},
|
},
|
||||||
coral: {
|
coral: {
|
||||||
name: "Coral Light",
|
name: "Coral Light",
|
||||||
@@ -343,14 +459,46 @@ const StockThemes = {
|
|||||||
backgroundText: "#1a1c1e",
|
backgroundText: "#1a1c1e",
|
||||||
outline: "#79747e",
|
outline: "#79747e",
|
||||||
surfaceContainer: "#f3f3f3",
|
surfaceContainer: "#f3f3f3",
|
||||||
surfaceContainerHigh: "#ececec"
|
surfaceContainerHigh: "#ececec",
|
||||||
|
surfaceContainerHighest: "#e6e6e6"
|
||||||
|
},
|
||||||
|
monochrome: {
|
||||||
|
name: "Monochrome Light",
|
||||||
|
primary: "#2b303c",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#d6d7dc",
|
||||||
|
secondary: "#4a4d56",
|
||||||
|
surface: "#f5f5f6",
|
||||||
|
surfaceText: "#2a2a2a",
|
||||||
|
surfaceVariant: "#e0e0e2",
|
||||||
|
surfaceVariantText: "#424242",
|
||||||
|
surfaceTint: "#5a5f6e",
|
||||||
|
background: "#ffffff",
|
||||||
|
backgroundText: "#1a1a1a",
|
||||||
|
outline: "#757577",
|
||||||
|
surfaceContainer: "#f5f5f6",
|
||||||
|
surfaceContainerHigh: "#eaeaeb",
|
||||||
|
error: "#ba1a1a",
|
||||||
|
warning: "#f9e79f",
|
||||||
|
info: "#5d6475",
|
||||||
|
matugen_type: "scheme-monochrome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ThemeCategories = {
|
||||||
|
GENERIC: {
|
||||||
|
name: "Generic",
|
||||||
|
variants: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"]
|
||||||
|
},
|
||||||
|
CATPPUCCIN: {
|
||||||
|
name: "Catppuccin",
|
||||||
|
variants: Object.keys(CatppuccinVariants)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ThemeNames = {
|
const ThemeNames = {
|
||||||
BLUE: "blue",
|
BLUE: "blue",
|
||||||
DEEP_BLUE: "deepBlue",
|
|
||||||
PURPLE: "purple",
|
PURPLE: "purple",
|
||||||
GREEN: "green",
|
GREEN: "green",
|
||||||
ORANGE: "orange",
|
ORANGE: "orange",
|
||||||
@@ -359,6 +507,7 @@ const ThemeNames = {
|
|||||||
PINK: "pink",
|
PINK: "pink",
|
||||||
AMBER: "amber",
|
AMBER: "amber",
|
||||||
CORAL: "coral",
|
CORAL: "coral",
|
||||||
|
MONOCHROME: "monochrome",
|
||||||
DYNAMIC: "dynamic"
|
DYNAMIC: "dynamic"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,11 +515,18 @@ function isStockTheme(themeName) {
|
|||||||
return Object.keys(StockThemes.DARK).includes(themeName)
|
return Object.keys(StockThemes.DARK).includes(themeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCatppuccinVariant(themeName) {
|
||||||
|
return Object.keys(CatppuccinVariants).includes(themeName)
|
||||||
|
}
|
||||||
|
|
||||||
function getAvailableThemes(isLight = false) {
|
function getAvailableThemes(isLight = false) {
|
||||||
return isLight ? StockThemes.LIGHT : StockThemes.DARK
|
return isLight ? StockThemes.LIGHT : StockThemes.DARK
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThemeByName(themeName, isLight = false) {
|
function getThemeByName(themeName, isLight = false) {
|
||||||
|
if (isCatppuccinVariant(themeName)) {
|
||||||
|
return getCatppuccinTheme(themeName, isLight)
|
||||||
|
}
|
||||||
const themes = getAvailableThemes(isLight)
|
const themes = getAvailableThemes(isLight)
|
||||||
return themes[themeName] || themes.blue
|
return themes[themeName] || themes.blue
|
||||||
}
|
}
|
||||||
@@ -378,3 +534,11 @@ function getThemeByName(themeName, isLight = false) {
|
|||||||
function getAllThemeNames() {
|
function getAllThemeNames() {
|
||||||
return Object.keys(StockThemes.DARK)
|
return Object.keys(StockThemes.DARK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCatppuccinVariantNames() {
|
||||||
|
return Object.keys(CatppuccinVariants)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeCategories() {
|
||||||
|
return ThemeCategories
|
||||||
|
}
|
||||||
589
Common/Theme.qml
589
Common/Theme.qml
@@ -1,4 +1,5 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtCore
|
import QtCore
|
||||||
@@ -6,37 +7,88 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.UPower
|
import Quickshell.Services.UPower
|
||||||
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import "StockThemes.js" as StockThemes
|
import "StockThemes.js" as StockThemes
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
|
||||||
|
|
||||||
property string currentTheme: "blue"
|
property string currentTheme: "blue"
|
||||||
|
property string currentThemeCategory: "generic"
|
||||||
property bool isLightMode: false
|
property bool isLightMode: false
|
||||||
|
|
||||||
readonly property string dynamic: "dynamic"
|
readonly property string dynamic: "dynamic"
|
||||||
|
readonly property string custom : "custom"
|
||||||
|
|
||||||
readonly property string homeDir: {
|
readonly property string homeDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation))
|
||||||
const url = StandardPaths.writableLocation(StandardPaths.HomeLocation).toString()
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||||
return url.startsWith("file://") ? url.substring(7) : url
|
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "")
|
||||||
|
readonly property string wallpaperPath: {
|
||||||
|
if (typeof SessionData === "undefined") return ""
|
||||||
|
|
||||||
|
if (SessionData.perMonitorWallpaper) {
|
||||||
|
// Use first monitor's wallpaper for dynamic theming
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0) {
|
||||||
|
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
|
||||||
|
var wallpaperPath = firstMonitorWallpaper || SessionData.wallpaperPath
|
||||||
|
|
||||||
|
if (wallpaperPath && wallpaperPath.startsWith("we:")) {
|
||||||
|
return stateDir + "/we_screenshots/" + wallpaperPath.substring(3) + ".jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallpaperPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wallpaperPath = SessionData.wallpaperPath
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0 && wallpaperPath && wallpaperPath.startsWith("we:")) {
|
||||||
|
return stateDir + "/we_screenshots/" + wallpaperPath.substring(3) + ".jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallpaperPath
|
||||||
}
|
}
|
||||||
readonly property string configDir: {
|
readonly property string rawWallpaperPath: {
|
||||||
const url = StandardPaths.writableLocation(StandardPaths.ConfigLocation).toString()
|
if (typeof SessionData === "undefined") return ""
|
||||||
return url.startsWith("file://") ? url.substring(7) : url
|
|
||||||
|
if (SessionData.perMonitorWallpaper) {
|
||||||
|
// Use first monitor's wallpaper for dynamic theming
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0) {
|
||||||
|
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
|
||||||
|
return firstMonitorWallpaper || SessionData.wallpaperPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SessionData.wallpaperPath
|
||||||
}
|
}
|
||||||
readonly property string shellDir: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Common/", "")
|
|
||||||
readonly property string wallpaperPath: typeof SessionData !== "undefined" ? SessionData.wallpaperPath : ""
|
|
||||||
|
|
||||||
property bool matugenAvailable: false
|
property bool matugenAvailable: false
|
||||||
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
|
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
|
||||||
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
|
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
|
||||||
property bool systemThemeGenerationInProgress: false
|
property var workerRunning: false
|
||||||
property var matugenColors: ({})
|
property var matugenColors: ({})
|
||||||
property bool extractionRequested: false
|
property bool extractionRequested: false
|
||||||
property int colorUpdateTrigger: 0
|
property int colorUpdateTrigger: 0
|
||||||
property var customThemeData: null
|
property var customThemeData: null
|
||||||
|
|
||||||
|
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()) + "/dankshell"
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Quickshell.execDetached(["mkdir", "-p", stateDir])
|
||||||
|
matugenCheck.running = true
|
||||||
|
if (typeof SessionData !== "undefined")
|
||||||
|
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
|
||||||
|
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
||||||
|
switchTheme(SettingsData.currentThemeName, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getMatugenColor(path, fallback) {
|
function getMatugenColor(path, fallback) {
|
||||||
colorUpdateTrigger
|
colorUpdateTrigger
|
||||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
||||||
@@ -54,35 +106,61 @@ Singleton {
|
|||||||
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
|
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
|
||||||
} else if (currentTheme === dynamic) {
|
} else if (currentTheme === dynamic) {
|
||||||
return {
|
return {
|
||||||
primary: getMatugenColor("primary", "#42a5f5"),
|
"primary": getMatugenColor("primary", "#42a5f5"),
|
||||||
primaryText: getMatugenColor("on_primary", "#ffffff"),
|
"primaryText": getMatugenColor("on_primary", "#ffffff"),
|
||||||
primaryContainer: getMatugenColor("primary_container", "#1976d2"),
|
"primaryContainer": getMatugenColor("primary_container", "#1976d2"),
|
||||||
secondary: getMatugenColor("secondary", "#8ab4f8"),
|
"secondary": getMatugenColor("secondary", "#8ab4f8"),
|
||||||
surface: getMatugenColor("surface", "#1a1c1e"),
|
"surface": getMatugenColor("surface", "#1a1c1e"),
|
||||||
surfaceText: getMatugenColor("on_background", "#e3e8ef"),
|
"surfaceText": getMatugenColor("on_background", "#e3e8ef"),
|
||||||
surfaceVariant: getMatugenColor("surface_variant", "#44464f"),
|
"surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
|
||||||
surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5"),
|
"surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"),
|
||||||
surfaceTint: getMatugenColor("surface_tint", "#8ab4f8"),
|
"surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"),
|
||||||
background: getMatugenColor("background", "#1a1c1e"),
|
"background": getMatugenColor("background", "#1a1c1e"),
|
||||||
backgroundText: getMatugenColor("on_background", "#e3e8ef"),
|
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
|
||||||
outline: getMatugenColor("outline", "#8e918f"),
|
"outline": getMatugenColor("outline", "#8e918f"),
|
||||||
surfaceContainer: getMatugenColor("surface_container", "#1e2023"),
|
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
|
||||||
surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f"),
|
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
|
||||||
error: "#F2B8B5",
|
"surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
|
||||||
warning: "#FF9800",
|
"error": "#F2B8B5",
|
||||||
info: "#2196F3",
|
"warning": "#FF9800",
|
||||||
success: "#4CAF50"
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return StockThemes.getThemeByName(currentTheme, isLightMode)
|
return StockThemes.getThemeByName(currentTheme, isLightMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property var availableMatugenSchemes: [
|
||||||
|
({ "value": "scheme-tonal-spot", "label": "Tonal Spot", "description": "Balanced palette with focused accents (default)." }),
|
||||||
|
({ "value": "scheme-content", "label": "Content", "description": "Derives colors that closely match the underlying image." }),
|
||||||
|
({ "value": "scheme-expressive", "label": "Expressive", "description": "Vibrant palette with playful saturation." }),
|
||||||
|
({ "value": "scheme-fidelity", "label": "Fidelity", "description": "High-fidelity palette that preserves source hues." }),
|
||||||
|
({ "value": "scheme-fruit-salad", "label": "Fruit Salad", "description": "Colorful mix of bright contrasting accents." }),
|
||||||
|
({ "value": "scheme-monochrome", "label": "Monochrome", "description": "Minimal palette built around a single hue." }),
|
||||||
|
({ "value": "scheme-neutral", "label": "Neutral", "description": "Muted palette with subdued, calming tones." }),
|
||||||
|
({ "value": "scheme-rainbow", "label": "Rainbow", "description": "Diverse palette spanning the full spectrum." })
|
||||||
|
]
|
||||||
|
|
||||||
|
function getMatugenScheme(value) {
|
||||||
|
const schemes = availableMatugenSchemes
|
||||||
|
for (let i = 0; i < schemes.length; i++) {
|
||||||
|
if (schemes[i].value === value)
|
||||||
|
return schemes[i]
|
||||||
|
}
|
||||||
|
return schemes[0]
|
||||||
|
}
|
||||||
|
|
||||||
property color primary: currentThemeData.primary
|
property color primary: currentThemeData.primary
|
||||||
property color primaryText: currentThemeData.primaryText
|
property color primaryText: currentThemeData.primaryText
|
||||||
property color primaryContainer: currentThemeData.primaryContainer
|
property color primaryContainer: currentThemeData.primaryContainer
|
||||||
property color secondary: currentThemeData.secondary
|
property color secondary: currentThemeData.secondary
|
||||||
property color surface: currentThemeData.surface
|
property color surface: {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||||
|
return currentThemeData.background
|
||||||
|
}
|
||||||
|
return currentThemeData.surface
|
||||||
|
}
|
||||||
property color surfaceText: currentThemeData.surfaceText
|
property color surfaceText: currentThemeData.surfaceText
|
||||||
property color surfaceVariant: currentThemeData.surfaceVariant
|
property color surfaceVariant: currentThemeData.surfaceVariant
|
||||||
property color surfaceVariantText: currentThemeData.surfaceVariantText
|
property color surfaceVariantText: currentThemeData.surfaceVariantText
|
||||||
@@ -90,8 +168,32 @@ Singleton {
|
|||||||
property color background: currentThemeData.background
|
property color background: currentThemeData.background
|
||||||
property color backgroundText: currentThemeData.backgroundText
|
property color backgroundText: currentThemeData.backgroundText
|
||||||
property color outline: currentThemeData.outline
|
property color outline: currentThemeData.outline
|
||||||
property color surfaceContainer: currentThemeData.surfaceContainer
|
property color outlineVariant: currentThemeData.outlineVariant || Qt.rgba(outline.r, outline.g, outline.b, 0.6)
|
||||||
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
|
property color surfaceContainer: {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||||
|
return currentThemeData.surface
|
||||||
|
}
|
||||||
|
return currentThemeData.surfaceContainer
|
||||||
|
}
|
||||||
|
property color surfaceContainerHigh: {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||||
|
return currentThemeData.surfaceContainer
|
||||||
|
}
|
||||||
|
return currentThemeData.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
property color surfaceContainerHighest: {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||||
|
return currentThemeData.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
return currentThemeData.surfaceContainerHighest
|
||||||
|
}
|
||||||
|
|
||||||
|
property color onSurface: surfaceText
|
||||||
|
property color onSurfaceVariant: surfaceVariantText
|
||||||
|
property color onPrimary: primaryText
|
||||||
|
property color onSurface_12: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.12)
|
||||||
|
property color onSurface_38: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.38)
|
||||||
|
property color onSurfaceVariant_30: Qt.rgba(onSurfaceVariant.r, onSurfaceVariant.g, onSurfaceVariant.b, 0.30)
|
||||||
|
|
||||||
property color error: currentThemeData.error || "#F2B8B5"
|
property color error: currentThemeData.error || "#F2B8B5"
|
||||||
property color warning: currentThemeData.warning || "#FF9800"
|
property color warning: currentThemeData.warning || "#FF9800"
|
||||||
@@ -124,10 +226,12 @@ Singleton {
|
|||||||
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
|
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
|
||||||
|
|
||||||
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
||||||
|
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
||||||
|
|
||||||
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
||||||
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
|
||||||
|
property int shorterDuration: 100
|
||||||
property int shortDuration: 150
|
property int shortDuration: 150
|
||||||
property int mediumDuration: 300
|
property int mediumDuration: 300
|
||||||
property int longDuration: 500
|
property int longDuration: 500
|
||||||
@@ -141,10 +245,10 @@ Singleton {
|
|||||||
property real spacingM: 12
|
property real spacingM: 12
|
||||||
property real spacingL: 16
|
property real spacingL: 16
|
||||||
property real spacingXL: 24
|
property real spacingXL: 24
|
||||||
property real fontSizeSmall: 12
|
property real fontSizeSmall: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 12
|
||||||
property real fontSizeMedium: 14
|
property real fontSizeMedium: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 14
|
||||||
property real fontSizeLarge: 16
|
property real fontSizeLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 16
|
||||||
property real fontSizeXLarge: 20
|
property real fontSizeXLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 20
|
||||||
property real barHeight: 48
|
property real barHeight: 48
|
||||||
property real iconSize: 24
|
property real iconSize: 24
|
||||||
property real iconSizeSmall: 16
|
property real iconSizeSmall: 16
|
||||||
@@ -154,17 +258,32 @@ Singleton {
|
|||||||
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85
|
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85
|
||||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92
|
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92
|
||||||
|
|
||||||
function switchTheme(themeName, savePrefs = true) {
|
function screenTransition() {
|
||||||
|
CompositorService.isNiri && NiriService.doScreenTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
||||||
|
if (enableTransition) {
|
||||||
|
screenTransition()
|
||||||
|
}
|
||||||
if (themeName === dynamic) {
|
if (themeName === dynamic) {
|
||||||
currentTheme = dynamic
|
currentTheme = dynamic
|
||||||
|
currentThemeCategory = dynamic
|
||||||
extractColors()
|
extractColors()
|
||||||
} else if (themeName === "custom") {
|
} else if (themeName === custom) {
|
||||||
currentTheme = "custom"
|
currentTheme = custom
|
||||||
|
currentThemeCategory = custom
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
|
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
|
||||||
loadCustomThemeFromFile(SettingsData.customThemeFile)
|
loadCustomThemeFromFile(SettingsData.customThemeFile)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentTheme = themeName
|
currentTheme = themeName
|
||||||
|
// Determine category based on theme name
|
||||||
|
if (StockThemes.isCatppuccinVariant(themeName)) {
|
||||||
|
currentThemeCategory = "catppuccin"
|
||||||
|
} else {
|
||||||
|
currentThemeCategory = "generic"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (savePrefs && typeof SettingsData !== "undefined")
|
if (savePrefs && typeof SettingsData !== "undefined")
|
||||||
SettingsData.setTheme(currentTheme)
|
SettingsData.setTheme(currentTheme)
|
||||||
@@ -172,18 +291,22 @@ Singleton {
|
|||||||
generateSystemThemesFromCurrentTheme()
|
generateSystemThemesFromCurrentTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLightMode(savePrefs = true) {
|
function setLightMode(light, savePrefs = true) {
|
||||||
isLightMode = !isLightMode
|
screenTransition()
|
||||||
|
isLightMode = light
|
||||||
if (savePrefs && typeof SessionData !== "undefined")
|
if (savePrefs && typeof SessionData !== "undefined")
|
||||||
SessionData.setLightMode(isLightMode)
|
SessionData.setLightMode(isLightMode)
|
||||||
|
PortalService.setLightMode(isLightMode)
|
||||||
generateSystemThemesFromCurrentTheme()
|
generateSystemThemesFromCurrentTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleLightMode(savePrefs = true) {
|
||||||
|
setLightMode(!isLightMode, savePrefs)
|
||||||
|
}
|
||||||
|
|
||||||
function forceGenerateSystemThemes() {
|
function forceGenerateSystemThemes() {
|
||||||
|
screenTransition()
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
if (typeof ToastService !== "undefined") {
|
|
||||||
ToastService.showWarning("matugen not available - cannot generate system themes")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
generateSystemThemesFromCurrentTheme()
|
generateSystemThemesFromCurrentTheme()
|
||||||
@@ -205,7 +328,33 @@ Singleton {
|
|||||||
return StockThemes.getThemeByName(themeName, isLightMode)
|
return StockThemes.getThemeByName(themeName, isLightMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchThemeCategory(category, defaultTheme) {
|
||||||
|
currentThemeCategory = category
|
||||||
|
switchTheme(defaultTheme, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCatppuccinColor(variantName) {
|
||||||
|
const catColors = {
|
||||||
|
"cat-rosewater": "#f5e0dc", "cat-flamingo": "#f2cdcd", "cat-pink": "#f5c2e7", "cat-mauve": "#cba6f7",
|
||||||
|
"cat-red": "#f38ba8", "cat-maroon": "#eba0ac", "cat-peach": "#fab387", "cat-yellow": "#f9e2af",
|
||||||
|
"cat-green": "#a6e3a1", "cat-teal": "#94e2d5", "cat-sky": "#89dceb", "cat-sapphire": "#74c7ec",
|
||||||
|
"cat-blue": "#89b4fa", "cat-lavender": "#b4befe"
|
||||||
|
}
|
||||||
|
return catColors[variantName] || "#cba6f7"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCatppuccinVariantName(variantName) {
|
||||||
|
const catNames = {
|
||||||
|
"cat-rosewater": "Rosewater", "cat-flamingo": "Flamingo", "cat-pink": "Pink", "cat-mauve": "Mauve",
|
||||||
|
"cat-red": "Red", "cat-maroon": "Maroon", "cat-peach": "Peach", "cat-yellow": "Yellow",
|
||||||
|
"cat-green": "Green", "cat-teal": "Teal", "cat-sky": "Sky", "cat-sapphire": "Sapphire",
|
||||||
|
"cat-blue": "Blue", "cat-lavender": "Lavender"
|
||||||
|
}
|
||||||
|
return catNames[variantName] || "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
function loadCustomTheme(themeData) {
|
function loadCustomTheme(themeData) {
|
||||||
|
screenTransition()
|
||||||
if (themeData.dark || themeData.light) {
|
if (themeData.dark || themeData.light) {
|
||||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
||||||
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
|
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
|
||||||
@@ -225,7 +374,6 @@ Singleton {
|
|||||||
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
|
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
|
||||||
property string currentThemeName: currentTheme
|
property string currentThemeName: currentTheme
|
||||||
|
|
||||||
|
|
||||||
function popupBackground() {
|
function popupBackground() {
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
||||||
}
|
}
|
||||||
@@ -238,8 +386,42 @@ Singleton {
|
|||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
|
||||||
}
|
}
|
||||||
|
|
||||||
function widgetBackground() {
|
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
|
||||||
|
property var widgetBaseBackgroundColor: {
|
||||||
|
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
|
||||||
|
switch (colorMode) {
|
||||||
|
case "s":
|
||||||
|
return surface
|
||||||
|
case "sc":
|
||||||
|
return surfaceContainer
|
||||||
|
case "sch":
|
||||||
|
return surfaceContainerHigh
|
||||||
|
case "sth":
|
||||||
|
default:
|
||||||
|
return surfaceTextHover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var widgetBaseHoverColor: {
|
||||||
|
const baseColor = widgetBaseBackgroundColor
|
||||||
|
const factor = 1.2
|
||||||
|
return isLightMode ? Qt.darker(baseColor, factor) : Qt.lighter(baseColor, factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
property var widgetBackground: {
|
||||||
|
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
|
||||||
|
switch (colorMode) {
|
||||||
|
case "s":
|
||||||
|
return Qt.rgba(surface.r, surface.g, surface.b, widgetTransparency)
|
||||||
|
case "sc":
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
||||||
|
case "sch":
|
||||||
|
return Qt.rgba(surfaceContainerHigh.r, surfaceContainerHigh.g, surfaceContainerHigh.b, widgetTransparency)
|
||||||
|
case "sth":
|
||||||
|
default:
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPopupBackgroundAlpha() {
|
function getPopupBackgroundAlpha() {
|
||||||
@@ -259,21 +441,34 @@ Singleton {
|
|||||||
return _getBatteryPowerProfileIcon()
|
return _getBatteryPowerProfileIcon()
|
||||||
|
|
||||||
if (isCharging) {
|
if (isCharging) {
|
||||||
if (level >= 90) return "battery_charging_full"
|
if (level >= 90)
|
||||||
if (level >= 80) return "battery_charging_90"
|
return "battery_charging_full"
|
||||||
if (level >= 60) return "battery_charging_80"
|
if (level >= 80)
|
||||||
if (level >= 50) return "battery_charging_60"
|
return "battery_charging_90"
|
||||||
if (level >= 30) return "battery_charging_50"
|
if (level >= 60)
|
||||||
if (level >= 20) return "battery_charging_30"
|
return "battery_charging_80"
|
||||||
|
if (level >= 50)
|
||||||
|
return "battery_charging_60"
|
||||||
|
if (level >= 30)
|
||||||
|
return "battery_charging_50"
|
||||||
|
if (level >= 20)
|
||||||
|
return "battery_charging_30"
|
||||||
return "battery_charging_20"
|
return "battery_charging_20"
|
||||||
} else {
|
} else {
|
||||||
if (level >= 95) return "battery_full"
|
if (level >= 95)
|
||||||
if (level >= 85) return "battery_6_bar"
|
return "battery_full"
|
||||||
if (level >= 70) return "battery_5_bar"
|
if (level >= 85)
|
||||||
if (level >= 55) return "battery_4_bar"
|
return "battery_6_bar"
|
||||||
if (level >= 40) return "battery_3_bar"
|
if (level >= 70)
|
||||||
if (level >= 25) return "battery_2_bar"
|
return "battery_5_bar"
|
||||||
if (level >= 10) return "battery_1_bar"
|
if (level >= 55)
|
||||||
|
return "battery_4_bar"
|
||||||
|
if (level >= 40)
|
||||||
|
return "battery_3_bar"
|
||||||
|
if (level >= 25)
|
||||||
|
return "battery_2_bar"
|
||||||
|
if (level >= 10)
|
||||||
|
return "battery_1_bar"
|
||||||
return "battery_alert"
|
return "battery_alert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,7 +529,11 @@ Singleton {
|
|||||||
function extractColors() {
|
function extractColors() {
|
||||||
extractionRequested = true
|
extractionRequested = true
|
||||||
if (matugenAvailable)
|
if (matugenAvailable)
|
||||||
fileChecker.running = true
|
if (rawWallpaperPath.startsWith("we:")) {
|
||||||
|
fileCheckerTimer.start()
|
||||||
|
} else {
|
||||||
|
fileChecker.running = true
|
||||||
|
}
|
||||||
else
|
else
|
||||||
matugenCheck.running = true
|
matugenCheck.running = true
|
||||||
}
|
}
|
||||||
@@ -347,59 +546,88 @@ Singleton {
|
|||||||
if (currentTheme === "custom" && customThemeFileView.path) {
|
if (currentTheme === "custom" && customThemeFileView.path) {
|
||||||
customThemeFileView.reload()
|
customThemeFileView.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSystemThemes() {
|
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType) {
|
||||||
if (systemThemeGenerationInProgress || !matugenAvailable || !wallpaperPath)
|
if (!matugenAvailable) {
|
||||||
|
console.warn("matugen not available or disabled - cannot set system theme")
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
|
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
NiriService.suppressNextToast()
|
||||||
|
}
|
||||||
|
|
||||||
systemThemeGenerationInProgress = true
|
const desired = {
|
||||||
systemThemeGenerator.command = [shellDir + "/scripts/matugen.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme]
|
"kind": kind,
|
||||||
|
"value": value,
|
||||||
|
"mode": isLight ? "light" : "dark",
|
||||||
|
"iconTheme": iconTheme || "System Default",
|
||||||
|
"matugenType": matugenType || "scheme-tonal-spot",
|
||||||
|
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc"
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = JSON.stringify(desired)
|
||||||
|
const desiredPath = stateDir + "/matugen.desired.json"
|
||||||
|
|
||||||
|
Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`])
|
||||||
|
workerRunning = true
|
||||||
|
if (rawWallpaperPath.startsWith("we:")) {
|
||||||
|
console.log("calling matugen worker")
|
||||||
|
systemThemeGenerator.command = [
|
||||||
|
"sh", "-c",
|
||||||
|
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' --run`
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"]
|
||||||
|
}
|
||||||
systemThemeGenerator.running = true
|
systemThemeGenerator.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSystemThemesFromCurrentTheme() {
|
function generateSystemThemesFromCurrentTheme() {
|
||||||
if (systemThemeGenerationInProgress || !matugenAvailable)
|
if (!matugenAvailable)
|
||||||
return
|
return
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||||
|
|
||||||
if (currentTheme === dynamic) {
|
if (currentTheme === dynamic) {
|
||||||
if (!wallpaperPath)
|
if (!wallpaperPath) {
|
||||||
return
|
return
|
||||||
systemThemeGenerationInProgress = true
|
}
|
||||||
systemThemeGenerator.command = [shellDir + "/scripts/matugen.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme]
|
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||||
systemThemeGenerator.running = true
|
if (wallpaperPath.startsWith("#")) {
|
||||||
|
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||||
|
} else {
|
||||||
|
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let primaryColor
|
let primaryColor
|
||||||
|
let matugenType
|
||||||
if (currentTheme === "custom") {
|
if (currentTheme === "custom") {
|
||||||
if (!customThemeData || !customThemeData.primary) {
|
if (!customThemeData || !customThemeData.primary) {
|
||||||
console.warn("Custom theme data not available for system theme generation")
|
console.warn("Custom theme data not available for system theme generation")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
primaryColor = customThemeData.primary
|
primaryColor = customThemeData.primary
|
||||||
|
matugenType = customThemeData.matugen_type
|
||||||
} else {
|
} else {
|
||||||
primaryColor = currentThemeData.primary
|
primaryColor = currentThemeData.primary
|
||||||
|
matugenType = currentThemeData.matugen_type
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!primaryColor)
|
if (!primaryColor) {
|
||||||
|
console.warn("No primary color available for theme:", currentTheme)
|
||||||
return
|
return
|
||||||
systemThemeGenerationInProgress = true
|
}
|
||||||
systemThemeGenerator.command = [shellDir + "/scripts/matugen.sh", primaryColor, shellDir, configDir, "generate-color", isLight, iconTheme]
|
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
||||||
systemThemeGenerator.running = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyGtkColors() {
|
function applyGtkColors() {
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
if (typeof ToastService !== "undefined") {
|
if (typeof ToastService !== "undefined") {
|
||||||
ToastService.showError("matugen not available - cannot apply GTK colors")
|
ToastService.showError("matugen not available or disabled - cannot apply GTK colors")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -412,7 +640,7 @@ Singleton {
|
|||||||
function applyQtColors() {
|
function applyQtColors() {
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
if (typeof ToastService !== "undefined") {
|
if (typeof ToastService !== "undefined") {
|
||||||
ToastService.showError("matugen not available - cannot apply Qt colors")
|
ToastService.showError("matugen not available or disabled - cannot apply Qt colors")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -421,17 +649,22 @@ Singleton {
|
|||||||
qtApplier.running = true
|
qtApplier.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function extractJsonFromText(text) {
|
function extractJsonFromText(text) {
|
||||||
if (!text) return null
|
if (!text)
|
||||||
|
return null
|
||||||
|
|
||||||
const start = text.search(/[{\[]/)
|
const start = text.search(/[{\[]/)
|
||||||
if (start === -1) return null
|
if (start === -1)
|
||||||
|
return null
|
||||||
|
|
||||||
const open = text[start]
|
const open = text[start]
|
||||||
const pairs = { "{": '}', "[": ']' }
|
const pairs = {
|
||||||
|
"{": '}',
|
||||||
|
"[": ']'
|
||||||
|
}
|
||||||
const close = pairs[open]
|
const close = pairs[open]
|
||||||
if (!close) return null
|
if (!close)
|
||||||
|
return null
|
||||||
|
|
||||||
let inString = false
|
let inString = false
|
||||||
let escape = false
|
let escape = false
|
||||||
@@ -476,16 +709,49 @@ Singleton {
|
|||||||
id: matugenCheck
|
id: matugenCheck
|
||||||
command: ["which", "matugen"]
|
command: ["which", "matugen"]
|
||||||
onExited: code => {
|
onExited: code => {
|
||||||
matugenAvailable = (code === 0)
|
matugenAvailable = (code === 0) && !envDisableMatugen
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
if (typeof ToastService !== "undefined") {
|
console.log("matugen not not available in path or disabled via DMS_DISABLE_MATUGEN")
|
||||||
ToastService.wallpaperErrorStatus = "matugen_missing"
|
|
||||||
ToastService.showWarning("matugen not found - dynamic theming disabled")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (extractionRequested) {
|
if (extractionRequested) {
|
||||||
fileChecker.running = true
|
if (rawWallpaperPath.startsWith("we:")) {
|
||||||
|
fileCheckerTimer.start()
|
||||||
|
} else {
|
||||||
|
fileChecker.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||||
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||||
|
|
||||||
|
if (currentTheme === dynamic) {
|
||||||
|
if (wallpaperPath) {
|
||||||
|
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||||
|
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||||
|
if (wallpaperPath.startsWith("#")) {
|
||||||
|
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||||
|
} else {
|
||||||
|
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let primaryColor
|
||||||
|
let matugenType
|
||||||
|
if (currentTheme === "custom") {
|
||||||
|
if (customThemeData && customThemeData.primary) {
|
||||||
|
primaryColor = customThemeData.primary
|
||||||
|
matugenType = customThemeData.matugen_type
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
primaryColor = currentThemeData.primary
|
||||||
|
matugenType = currentThemeData.matugen_type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryColor) {
|
||||||
|
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||||
|
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,18 +762,27 @@ Singleton {
|
|||||||
onExited: code => {
|
onExited: code => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
matugenProcess.running = true
|
matugenProcess.running = true
|
||||||
} else {
|
} else if (wallpaperPath.startsWith("#")) {
|
||||||
if (typeof ToastService !== "undefined") {
|
colorMatugenProcess.running = true
|
||||||
ToastService.wallpaperErrorStatus = "error"
|
|
||||||
ToastService.showError("Wallpaper processing failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: fileCheckerTimer
|
||||||
|
interval: 1000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
fileChecker.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: matugenProcess
|
id: matugenProcess
|
||||||
command: ["matugen", "image", wallpaperPath, "--json", "hex"]
|
command: {
|
||||||
|
const scheme = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||||
|
return ["matugen", "image", wallpaperPath, "--json", "hex", "-t", scheme]
|
||||||
|
}
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
id: matugenCollector
|
id: matugenCollector
|
||||||
@@ -531,7 +806,6 @@ Singleton {
|
|||||||
try {
|
try {
|
||||||
root.matugenColors = JSON.parse(extractedJson)
|
root.matugenColors = JSON.parse(extractedJson)
|
||||||
root.colorUpdateTrigger++
|
root.colorUpdateTrigger++
|
||||||
generateAppConfigs()
|
|
||||||
if (typeof ToastService !== "undefined") {
|
if (typeof ToastService !== "undefined") {
|
||||||
ToastService.clearWallpaperError()
|
ToastService.clearWallpaperError()
|
||||||
}
|
}
|
||||||
@@ -554,24 +828,76 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: colorMatugenProcess
|
||||||
|
command: {
|
||||||
|
const scheme = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||||
|
return ["matugen", "color", "hex", wallpaperPath, "--json", "hex", "-t", scheme]
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: colorMatugenCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
if (!colorMatugenCollector.text) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Color Processing Failed: Empty JSON extracted from matugen output.")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const extractedJson = extractJsonFromText(colorMatugenCollector.text)
|
||||||
|
if (!extractedJson) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Color Processing Failed: Invalid JSON extracted from matugen output.")
|
||||||
|
}
|
||||||
|
console.log("Raw matugen output:", colorMatugenCollector.text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
root.matugenColors = JSON.parse(extractedJson)
|
||||||
|
root.colorUpdateTrigger++
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.clearWallpaperError()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Color processing failed (JSON parse error after extraction)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: code => {
|
||||||
|
if (code !== 0) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Matugen color command failed with exit code " + code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: ensureStateDir
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: systemThemeGenerator
|
id: systemThemeGenerator
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
id: systemThemeStdout
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
id: systemThemeStderr
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
systemThemeGenerationInProgress = false
|
workerRunning = false
|
||||||
if (exitCode !== 0) {
|
|
||||||
|
if (exitCode === 2) {
|
||||||
|
// Exit code 2 means wallpaper/color not found - this is expected on first run
|
||||||
|
console.log("Theme worker: wallpaper/color not found, skipping theme generation")
|
||||||
|
} else if (exitCode !== 0) {
|
||||||
if (typeof ToastService !== "undefined") {
|
if (typeof ToastService !== "undefined") {
|
||||||
ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text)
|
ToastService.showError("Theme worker failed (" + exitCode + ")")
|
||||||
}
|
}
|
||||||
|
console.warn("Theme worker failed with exit code:", exitCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -590,7 +916,7 @@ Singleton {
|
|||||||
|
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
if (exitCode === 0) {
|
if (exitCode === 0) {
|
||||||
if (typeof ToastService !== "undefined") {
|
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
|
||||||
ToastService.showInfo("GTK colors applied successfully")
|
ToastService.showInfo("GTK colors applied successfully")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -626,26 +952,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function generateAppConfigs() {
|
|
||||||
if (!matugenColors || !matugenColors.colors) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
generateSystemThemes()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
matugenCheck.running = true
|
|
||||||
if (typeof SessionData !== "undefined")
|
|
||||||
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
|
|
||||||
|
|
||||||
// Generate system themes on startup for current theme
|
|
||||||
Qt.callLater(generateSystemThemesFromCurrentTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: customThemeFileView
|
id: customThemeFileView
|
||||||
watchChanges: currentTheme === "custom"
|
watchChanges: currentTheme === "custom"
|
||||||
@@ -667,10 +973,33 @@ Singleton {
|
|||||||
customThemeFileView.reload()
|
customThemeFileView.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoadFailed: function(error) {
|
onLoadFailed: function (error) {
|
||||||
if (typeof ToastService !== "undefined") {
|
if (typeof ToastService !== "undefined") {
|
||||||
ToastService.showError("Failed to read theme file: " + error)
|
ToastService.showError("Failed to read theme file: " + error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "theme"
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
root.toggleLightMode()
|
||||||
|
return root.isLightMode ? "light" : "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
function light(): string {
|
||||||
|
root.setLightMode(true)
|
||||||
|
return "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
function dark(): string {
|
||||||
|
root.setLightMode(false)
|
||||||
|
return "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMode(): string {
|
||||||
|
return root.isLightMode ? "light" : "dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,678 +0,0 @@
|
|||||||
.pragma library
|
|
||||||
|
|
||||||
var single = (search, target) => {
|
|
||||||
if(!search || !target) return NULL
|
|
||||||
|
|
||||||
var preparedSearch = getPreparedSearch(search)
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
|
|
||||||
var searchBitflags = preparedSearch.bitflags
|
|
||||||
if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
|
|
||||||
|
|
||||||
return algorithm(preparedSearch, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
var go = (search, targets, options) => {
|
|
||||||
if(!search) return options?.all ? all(targets, options) : noResults
|
|
||||||
|
|
||||||
var preparedSearch = getPreparedSearch(search)
|
|
||||||
var searchBitflags = preparedSearch.bitflags
|
|
||||||
var containsSpace = preparedSearch.containsSpace
|
|
||||||
|
|
||||||
var threshold = denormalizeScore( options?.threshold || 0 )
|
|
||||||
var limit = options?.limit || INFINITY
|
|
||||||
|
|
||||||
var resultsLen = 0; var limitedCount = 0
|
|
||||||
var targetsLen = targets.length
|
|
||||||
|
|
||||||
function push_result(result) {
|
|
||||||
if(resultsLen < limit) { q.add(result); ++resultsLen }
|
|
||||||
else {
|
|
||||||
++limitedCount
|
|
||||||
if(result._score > q.peek()._score) q.replaceTop(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
|
|
||||||
|
|
||||||
// options.key
|
|
||||||
if(options?.key) {
|
|
||||||
var key = options.key
|
|
||||||
for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
|
||||||
var target = getValue(obj, key)
|
|
||||||
if(!target) continue
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
|
|
||||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
|
||||||
var result = algorithm(preparedSearch, target)
|
|
||||||
if(result === NULL) continue
|
|
||||||
if(result._score < threshold) continue
|
|
||||||
|
|
||||||
result.obj = obj
|
|
||||||
push_result(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// options.keys
|
|
||||||
} else if(options?.keys) {
|
|
||||||
var keys = options.keys
|
|
||||||
var keysLen = keys.length
|
|
||||||
|
|
||||||
outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
|
||||||
|
|
||||||
{ // early out based on bitflags
|
|
||||||
var keysBitflags = 0
|
|
||||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
|
||||||
var key = keys[keyI]
|
|
||||||
var target = getValue(obj, key)
|
|
||||||
if(!target) { tmpTargets[keyI] = noTarget; continue }
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
tmpTargets[keyI] = target
|
|
||||||
|
|
||||||
keysBitflags |= target._bitflags
|
|
||||||
}
|
|
||||||
|
|
||||||
if((searchBitflags & keysBitflags) !== searchBitflags) continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
|
|
||||||
|
|
||||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
|
||||||
target = tmpTargets[keyI]
|
|
||||||
if(target === noTarget) { tmpResults[keyI] = noTarget; continue }
|
|
||||||
|
|
||||||
tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
|
|
||||||
if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
|
|
||||||
|
|
||||||
// todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
|
|
||||||
// if our second match isn't good we ignore it instead of averaging with it
|
|
||||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {
|
|
||||||
if(allowPartialMatchScores[i] > -1000) {
|
|
||||||
if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
|
|
||||||
var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
|
|
||||||
if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(containsSpace) {
|
|
||||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
|
|
||||||
} else {
|
|
||||||
var hasAtLeast1Match = false
|
|
||||||
for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
|
|
||||||
if(!hasAtLeast1Match) continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var objResults = new KeysResult(keysLen)
|
|
||||||
for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
|
|
||||||
|
|
||||||
if(containsSpace) {
|
|
||||||
var score = 0
|
|
||||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
|
|
||||||
} else {
|
|
||||||
// todo could rewrite this scoring to be more similar to when there's spaces
|
|
||||||
// if we match multiple keys give us bonus points
|
|
||||||
var score = NEGATIVE_INFINITY
|
|
||||||
for(let i=0; i<keysLen; i++) {
|
|
||||||
var result = objResults[i]
|
|
||||||
if(result._score > -1000) {
|
|
||||||
if(score > NEGATIVE_INFINITY) {
|
|
||||||
var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
|
|
||||||
if(tmp > score) score = tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(result._score > score) score = result._score
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
objResults.obj = obj
|
|
||||||
objResults._score = score
|
|
||||||
if(options?.scoreFn) {
|
|
||||||
score = options.scoreFn(objResults)
|
|
||||||
if(!score) continue
|
|
||||||
score = denormalizeScore(score)
|
|
||||||
objResults._score = score
|
|
||||||
}
|
|
||||||
|
|
||||||
if(score < threshold) continue
|
|
||||||
push_result(objResults)
|
|
||||||
}
|
|
||||||
|
|
||||||
// no keys
|
|
||||||
} else {
|
|
||||||
for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
|
|
||||||
if(!target) continue
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
|
|
||||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
|
||||||
var result = algorithm(preparedSearch, target)
|
|
||||||
if(result === NULL) continue
|
|
||||||
if(result._score < threshold) continue
|
|
||||||
|
|
||||||
push_result(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(resultsLen === 0) return noResults
|
|
||||||
var results = new Array(resultsLen)
|
|
||||||
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
|
|
||||||
results.total = resultsLen + limitedCount
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// this is written as 1 function instead of 2 for minification. perf seems fine ...
|
|
||||||
// except when minified. the perf is very slow
|
|
||||||
var highlight = (result, open='<b>', close='</b>') => {
|
|
||||||
var callback = typeof open === 'function' ? open : undefined
|
|
||||||
|
|
||||||
var target = result.target
|
|
||||||
var targetLen = target.length
|
|
||||||
var indexes = result.indexes
|
|
||||||
var highlighted = ''
|
|
||||||
var matchI = 0
|
|
||||||
var indexesI = 0
|
|
||||||
var opened = false
|
|
||||||
var parts = []
|
|
||||||
|
|
||||||
for(var i = 0; i < targetLen; ++i) { var char = target[i]
|
|
||||||
if(indexes[indexesI] === i) {
|
|
||||||
++indexesI
|
|
||||||
if(!opened) { opened = true
|
|
||||||
if(callback) {
|
|
||||||
parts.push(highlighted); highlighted = ''
|
|
||||||
} else {
|
|
||||||
highlighted += open
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(indexesI === indexes.length) {
|
|
||||||
if(callback) {
|
|
||||||
highlighted += char
|
|
||||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
|
||||||
parts.push(target.substr(i+1))
|
|
||||||
} else {
|
|
||||||
highlighted += char + close + target.substr(i+1)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(opened) { opened = false
|
|
||||||
if(callback) {
|
|
||||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
|
||||||
} else {
|
|
||||||
highlighted += close
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
highlighted += char
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback ? parts : highlighted
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var prepare = (target) => {
|
|
||||||
if(typeof target === 'number') target = ''+target
|
|
||||||
else if(typeof target !== 'string') target = ''
|
|
||||||
var info = prepareLowerInfo(target)
|
|
||||||
return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
|
|
||||||
}
|
|
||||||
|
|
||||||
var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
|
|
||||||
|
|
||||||
|
|
||||||
// Below this point is only internal code
|
|
||||||
// Below this point is only internal code
|
|
||||||
// Below this point is only internal code
|
|
||||||
// Below this point is only internal code
|
|
||||||
|
|
||||||
|
|
||||||
class Result {
|
|
||||||
get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
|
|
||||||
set ['indexes'](indexes) { return this._indexes = indexes }
|
|
||||||
['highlight'](open, close) { return highlight(this, open, close) }
|
|
||||||
get ['score']() { return normalizeScore(this._score) }
|
|
||||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeysResult extends Array {
|
|
||||||
get ['score']() { return normalizeScore(this._score) }
|
|
||||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var new_result = (target, options) => {
|
|
||||||
const result = new Result()
|
|
||||||
result['target'] = target
|
|
||||||
result['obj'] = options.obj ?? NULL
|
|
||||||
result._score = options._score ?? NEGATIVE_INFINITY
|
|
||||||
result._indexes = options._indexes ?? []
|
|
||||||
result._targetLower = options._targetLower ?? ''
|
|
||||||
result._targetLowerCodes = options._targetLowerCodes ?? NULL
|
|
||||||
result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
|
|
||||||
result._bitflags = options._bitflags ?? 0
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var normalizeScore = score => {
|
|
||||||
if(score === NEGATIVE_INFINITY) return 0
|
|
||||||
if(score > 1) return score
|
|
||||||
return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
|
|
||||||
}
|
|
||||||
var denormalizeScore = normalizedScore => {
|
|
||||||
if(normalizedScore === 0) return NEGATIVE_INFINITY
|
|
||||||
if(normalizedScore > 1) return normalizedScore
|
|
||||||
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var prepareSearch = (search) => {
|
|
||||||
if(typeof search === 'number') search = ''+search
|
|
||||||
else if(typeof search !== 'string') search = ''
|
|
||||||
search = search.trim()
|
|
||||||
var info = prepareLowerInfo(search)
|
|
||||||
|
|
||||||
var spaceSearches = []
|
|
||||||
if(info.containsSpace) {
|
|
||||||
var searches = search.split(/\s+/)
|
|
||||||
searches = [...new Set(searches)] // distinct
|
|
||||||
for(var i=0; i<searches.length; i++) {
|
|
||||||
if(searches[i] === '') continue
|
|
||||||
var _info = prepareLowerInfo(searches[i])
|
|
||||||
spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var getPrepared = (target) => {
|
|
||||||
if(target.length > 999) return prepare(target) // don't cache huge targets
|
|
||||||
var targetPrepared = preparedCache.get(target)
|
|
||||||
if(targetPrepared !== undefined) return targetPrepared
|
|
||||||
targetPrepared = prepare(target)
|
|
||||||
preparedCache.set(target, targetPrepared)
|
|
||||||
return targetPrepared
|
|
||||||
}
|
|
||||||
var getPreparedSearch = (search) => {
|
|
||||||
if(search.length > 999) return prepareSearch(search) // don't cache huge searches
|
|
||||||
var searchPrepared = preparedSearchCache.get(search)
|
|
||||||
if(searchPrepared !== undefined) return searchPrepared
|
|
||||||
searchPrepared = prepareSearch(search)
|
|
||||||
preparedSearchCache.set(search, searchPrepared)
|
|
||||||
return searchPrepared
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var all = (targets, options) => {
|
|
||||||
var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
|
|
||||||
|
|
||||||
var limit = options?.limit || INFINITY
|
|
||||||
|
|
||||||
if(options?.key) {
|
|
||||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
|
||||||
var target = getValue(obj, options.key)
|
|
||||||
if(target == NULL) continue
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
var result = new_result(target.target, {_score: target._score, obj: obj})
|
|
||||||
results.push(result); if(results.length >= limit) return results
|
|
||||||
}
|
|
||||||
} else if(options?.keys) {
|
|
||||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
|
||||||
var objResults = new KeysResult(options.keys.length)
|
|
||||||
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
|
|
||||||
var target = getValue(obj, options.keys[keyI])
|
|
||||||
if(!target) { objResults[keyI] = noTarget; continue }
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
target._score = NEGATIVE_INFINITY
|
|
||||||
target._indexes.len = 0
|
|
||||||
objResults[keyI] = target
|
|
||||||
}
|
|
||||||
objResults.obj = obj
|
|
||||||
objResults._score = NEGATIVE_INFINITY
|
|
||||||
results.push(objResults); if(results.length >= limit) return results
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(var i=0;i<targets.length;i++) { var target = targets[i]
|
|
||||||
if(target == NULL) continue
|
|
||||||
if(!isPrepared(target)) target = getPrepared(target)
|
|
||||||
target._score = NEGATIVE_INFINITY
|
|
||||||
target._indexes.len = 0
|
|
||||||
results.push(target); if(results.length >= limit) return results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
|
|
||||||
if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
|
|
||||||
|
|
||||||
var searchLower = preparedSearch._lower
|
|
||||||
var searchLowerCodes = preparedSearch.lowerCodes
|
|
||||||
var searchLowerCode = searchLowerCodes[0]
|
|
||||||
var targetLowerCodes = prepared._targetLowerCodes
|
|
||||||
var searchLen = searchLowerCodes.length
|
|
||||||
var targetLen = targetLowerCodes.length
|
|
||||||
var searchI = 0 // where we at
|
|
||||||
var targetI = 0 // where you at
|
|
||||||
var matchesSimpleLen = 0
|
|
||||||
|
|
||||||
// very basic fuzzy match; to remove non-matching targets ASAP!
|
|
||||||
// walk through target. find sequential matches.
|
|
||||||
// if all chars aren't found then exit
|
|
||||||
for(;;) {
|
|
||||||
var isMatch = searchLowerCode === targetLowerCodes[targetI]
|
|
||||||
if(isMatch) {
|
|
||||||
matchesSimple[matchesSimpleLen++] = targetI
|
|
||||||
++searchI; if(searchI === searchLen) break
|
|
||||||
searchLowerCode = searchLowerCodes[searchI]
|
|
||||||
}
|
|
||||||
++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchI = 0
|
|
||||||
var successStrict = false
|
|
||||||
var matchesStrictLen = 0
|
|
||||||
|
|
||||||
var nextBeginningIndexes = prepared._nextBeginningIndexes
|
|
||||||
if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
|
|
||||||
targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
|
|
||||||
|
|
||||||
// Our target string successfully matched all characters in sequence!
|
|
||||||
// Let's try a more advanced and strict test to improve the score
|
|
||||||
// only count it as a match if it's consecutive or a beginning character!
|
|
||||||
var backtrackCount = 0
|
|
||||||
if(targetI !== targetLen) for(;;) {
|
|
||||||
if(targetI >= targetLen) {
|
|
||||||
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
|
|
||||||
if(searchI <= 0) break // We failed to push chars forward for a better match
|
|
||||||
|
|
||||||
++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
|
|
||||||
|
|
||||||
--searchI
|
|
||||||
var lastMatch = matchesStrict[--matchesStrictLen]
|
|
||||||
targetI = nextBeginningIndexes[lastMatch]
|
|
||||||
|
|
||||||
} else {
|
|
||||||
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
|
|
||||||
if(isMatch) {
|
|
||||||
matchesStrict[matchesStrictLen++] = targetI
|
|
||||||
++searchI; if(searchI === searchLen) { successStrict = true; break }
|
|
||||||
++targetI
|
|
||||||
} else {
|
|
||||||
targetI = nextBeginningIndexes[targetI]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it's a substring match
|
|
||||||
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
|
|
||||||
var isSubstring = !!~substringIndex
|
|
||||||
var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
|
|
||||||
|
|
||||||
// if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
|
|
||||||
if(isSubstring && !isSubstringBeginning) {
|
|
||||||
for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {
|
|
||||||
if(i <= substringIndex) continue
|
|
||||||
|
|
||||||
for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break
|
|
||||||
if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tally up the score & keep track of matches for highlighting later
|
|
||||||
// if it's a simple match, we'll switch to a substring match if a substring exists
|
|
||||||
// if it's a strict match, we'll switch to a substring match only if that's a better score
|
|
||||||
|
|
||||||
var calculateScore = matches => {
|
|
||||||
var score = 0
|
|
||||||
|
|
||||||
var extraMatchGroupCount = 0
|
|
||||||
for(var i = 1; i < searchLen; ++i) {
|
|
||||||
if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
|
|
||||||
}
|
|
||||||
var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
|
|
||||||
|
|
||||||
score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
|
|
||||||
|
|
||||||
if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
|
|
||||||
|
|
||||||
if(!successStrict) {
|
|
||||||
score *= 1000
|
|
||||||
} else {
|
|
||||||
// successStrict on a target with too many beginning indexes loses points for being a bad target
|
|
||||||
var uniqueBeginningIndexes = 1
|
|
||||||
for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
|
|
||||||
|
|
||||||
if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
|
|
||||||
}
|
|
||||||
|
|
||||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
|
||||||
|
|
||||||
if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
|
|
||||||
if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
|
|
||||||
|
|
||||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
|
||||||
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!successStrict) {
|
|
||||||
if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
|
||||||
var matchesBest = matchesSimple
|
|
||||||
var score = calculateScore(matchesBest)
|
|
||||||
} else {
|
|
||||||
if(isSubstringBeginning) {
|
|
||||||
for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
|
||||||
var matchesBest = matchesSimple
|
|
||||||
var score = calculateScore(matchesSimple)
|
|
||||||
} else {
|
|
||||||
var matchesBest = matchesStrict
|
|
||||||
var score = calculateScore(matchesStrict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prepared._score = score
|
|
||||||
|
|
||||||
for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
|
|
||||||
prepared._indexes.len = searchLen
|
|
||||||
|
|
||||||
const result = new Result()
|
|
||||||
result.target = prepared.target
|
|
||||||
result._score = prepared._score
|
|
||||||
result._indexes = prepared._indexes
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
|
|
||||||
var seen_indexes = new Set()
|
|
||||||
var score = 0
|
|
||||||
var result = NULL
|
|
||||||
|
|
||||||
var first_seen_index_last_search = 0
|
|
||||||
var searches = preparedSearch.spaceSearches
|
|
||||||
var searchesLen = searches.length
|
|
||||||
var changeslen = 0
|
|
||||||
|
|
||||||
// Return _nextBeginningIndexes back to its normal state
|
|
||||||
var resetNextBeginningIndexes = () => {
|
|
||||||
for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasAtLeast1Match = false
|
|
||||||
for(var i=0; i<searchesLen; ++i) {
|
|
||||||
allowPartialMatchScores[i] = NEGATIVE_INFINITY
|
|
||||||
var search = searches[i]
|
|
||||||
|
|
||||||
result = algorithm(search, target)
|
|
||||||
if(allowPartialMatch) {
|
|
||||||
if(result === NULL) continue
|
|
||||||
hasAtLeast1Match = true
|
|
||||||
} else {
|
|
||||||
if(result === NULL) {resetNextBeginningIndexes(); return NULL}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if not the last search, we need to mutate _nextBeginningIndexes for the next search
|
|
||||||
var isTheLastSearch = i === searchesLen - 1
|
|
||||||
if(!isTheLastSearch) {
|
|
||||||
var indexes = result._indexes
|
|
||||||
|
|
||||||
var indexesIsConsecutiveSubstring = true
|
|
||||||
for(let i=0; i<indexes.len-1; i++) {
|
|
||||||
if(indexes[i+1] - indexes[i] !== 1) {
|
|
||||||
indexesIsConsecutiveSubstring = false; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(indexesIsConsecutiveSubstring) {
|
|
||||||
var newBeginningIndex = indexes[indexes.len-1] + 1
|
|
||||||
var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]
|
|
||||||
for(let i=newBeginningIndex-1; i>=0; i--) {
|
|
||||||
if(toReplace !== target._nextBeginningIndexes[i]) break
|
|
||||||
target._nextBeginningIndexes[i] = newBeginningIndex
|
|
||||||
nextBeginningIndexesChanges[changeslen*2 + 0] = i
|
|
||||||
nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
|
|
||||||
changeslen++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
score += result._score / searchesLen
|
|
||||||
allowPartialMatchScores[i] = result._score / searchesLen
|
|
||||||
|
|
||||||
// dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
|
|
||||||
if(result._indexes[0] < first_seen_index_last_search) {
|
|
||||||
score -= (first_seen_index_last_search - result._indexes[0]) * 2
|
|
||||||
}
|
|
||||||
first_seen_index_last_search = result._indexes[0]
|
|
||||||
|
|
||||||
for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
|
|
||||||
}
|
|
||||||
|
|
||||||
if(allowPartialMatch && !hasAtLeast1Match) return NULL
|
|
||||||
|
|
||||||
resetNextBeginningIndexes()
|
|
||||||
|
|
||||||
// allows a search with spaces that's an exact substring to score well
|
|
||||||
var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
|
|
||||||
if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {
|
|
||||||
if(allowPartialMatch) {
|
|
||||||
for(var i=0; i<searchesLen; ++i) {
|
|
||||||
allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allowSpacesResult
|
|
||||||
}
|
|
||||||
|
|
||||||
if(allowPartialMatch) result = target
|
|
||||||
result._score = score
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
for (let index of seen_indexes) result._indexes[i++] = index
|
|
||||||
result._indexes.len = i
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
|
|
||||||
var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
|
|
||||||
|
|
||||||
var prepareLowerInfo = (str) => {
|
|
||||||
str = remove_accents(str)
|
|
||||||
var strLen = str.length
|
|
||||||
var lower = str.toLowerCase()
|
|
||||||
var lowerCodes = [] // new Array(strLen) sparse array is too slow
|
|
||||||
var bitflags = 0
|
|
||||||
var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
|
|
||||||
|
|
||||||
for(var i = 0; i < strLen; ++i) {
|
|
||||||
var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
|
|
||||||
|
|
||||||
if(lowerCode === 32) {
|
|
||||||
containsSpace = true
|
|
||||||
continue // it's important that we don't set any bitflags for space
|
|
||||||
}
|
|
||||||
|
|
||||||
var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
|
|
||||||
: lowerCode>=48&&lowerCode<=57 ? 26 // numbers
|
|
||||||
// 3 bits available
|
|
||||||
: lowerCode<=127 ? 30 // other ascii
|
|
||||||
: 31 // other utf8
|
|
||||||
bitflags |= 1<<bit
|
|
||||||
}
|
|
||||||
|
|
||||||
return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}
|
|
||||||
}
|
|
||||||
var prepareBeginningIndexes = (target) => {
|
|
||||||
var targetLen = target.length
|
|
||||||
var beginningIndexes = []; var beginningIndexesLen = 0
|
|
||||||
var wasUpper = false
|
|
||||||
var wasAlphanum = false
|
|
||||||
for(var i = 0; i < targetLen; ++i) {
|
|
||||||
var targetCode = target.charCodeAt(i)
|
|
||||||
var isUpper = targetCode>=65&&targetCode<=90
|
|
||||||
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
|
|
||||||
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
|
|
||||||
wasUpper = isUpper
|
|
||||||
wasAlphanum = isAlphanum
|
|
||||||
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
|
|
||||||
}
|
|
||||||
return beginningIndexes
|
|
||||||
}
|
|
||||||
var prepareNextBeginningIndexes = (target) => {
|
|
||||||
target = remove_accents(target)
|
|
||||||
var targetLen = target.length
|
|
||||||
var beginningIndexes = prepareBeginningIndexes(target)
|
|
||||||
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
|
|
||||||
var lastIsBeginning = beginningIndexes[0]
|
|
||||||
var lastIsBeginningI = 0
|
|
||||||
for(var i = 0; i < targetLen; ++i) {
|
|
||||||
if(lastIsBeginning > i) {
|
|
||||||
nextBeginningIndexes[i] = lastIsBeginning
|
|
||||||
} else {
|
|
||||||
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
|
|
||||||
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nextBeginningIndexes
|
|
||||||
}
|
|
||||||
|
|
||||||
var preparedCache = new Map()
|
|
||||||
var preparedSearchCache = new Map()
|
|
||||||
|
|
||||||
// the theory behind these being globals is to reduce garbage collection by not making new arrays
|
|
||||||
var matchesSimple = []; var matchesStrict = []
|
|
||||||
var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
|
|
||||||
var keysSpacesBestScores = []; var allowPartialMatchScores = []
|
|
||||||
var tmpTargets = []; var tmpResults = []
|
|
||||||
|
|
||||||
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
|
|
||||||
// prop = 'key1.key2' 10ms
|
|
||||||
// prop = ['key1', 'key2'] 27ms
|
|
||||||
// prop = obj => obj.tags.join() ??ms
|
|
||||||
var getValue = (obj, prop) => {
|
|
||||||
var tmp = obj[prop]; if(tmp !== undefined) return tmp
|
|
||||||
if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
|
|
||||||
var segs = prop
|
|
||||||
if(!Array.isArray(prop)) segs = prop.split('.')
|
|
||||||
var len = segs.length
|
|
||||||
var i = -1
|
|
||||||
while (obj && (++i < len)) obj = obj[segs[i]]
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
|
|
||||||
var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
|
|
||||||
var noResults = []; noResults.total = 0
|
|
||||||
var NULL = null
|
|
||||||
|
|
||||||
var noTarget = prepare('')
|
|
||||||
|
|
||||||
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
|
|
||||||
var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
|
|
||||||
var q = fastpriorityqueue() // reuse this
|
|
||||||
1307
Common/fzf.js
Normal file
1307
Common/fzf.js
Normal file
File diff suppressed because it is too large
Load Diff
19
Modals/Clipboard/ClipboardConstants.qml
Normal file
19
Modals/Clipboard/ClipboardConstants.qml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
readonly property int previewLength: 100
|
||||||
|
readonly property int longTextThreshold: 200
|
||||||
|
readonly property int modalWidth: 650
|
||||||
|
readonly property int modalHeight: 550
|
||||||
|
readonly property int itemHeight: 72
|
||||||
|
readonly property int thumbnailSize: 48
|
||||||
|
readonly property int retryInterval: 50
|
||||||
|
readonly property int viewportBuffer: 100
|
||||||
|
readonly property int extendedBuffer: 200
|
||||||
|
readonly property int keyboardHintsHeight: 80
|
||||||
|
readonly property int headerHeight: 40
|
||||||
|
}
|
||||||
169
Modals/Clipboard/ClipboardContent.qml
Normal file
169
Modals/Clipboard/ClipboardContent.qml
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: clipboardContent
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
required property var filteredModel
|
||||||
|
required property var clearConfirmDialog
|
||||||
|
|
||||||
|
property alias searchField: searchField
|
||||||
|
property alias clipboardListView: clipboardListView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
focus: false
|
||||||
|
|
||||||
|
// Header
|
||||||
|
ClipboardHeader {
|
||||||
|
id: header
|
||||||
|
width: parent.width
|
||||||
|
totalCount: modal.totalCount
|
||||||
|
showKeyboardHints: modal.showKeyboardHints
|
||||||
|
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||||
|
onClearAllClicked: {
|
||||||
|
clearConfirmDialog.show("Clear All History?", "This will permanently delete all clipboard history.", function () {
|
||||||
|
modal.clearAll()
|
||||||
|
modal.hide()
|
||||||
|
}, function () {})
|
||||||
|
}
|
||||||
|
onCloseClicked: modal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Field
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: ""
|
||||||
|
leftIconName: "search"
|
||||||
|
showClearButton: true
|
||||||
|
focus: true
|
||||||
|
ignoreLeftRightKeys: true
|
||||||
|
keyForwardTargets: [modal.modalFocusScope]
|
||||||
|
onTextChanged: {
|
||||||
|
modal.searchText = text
|
||||||
|
modal.updateFilteredModel()
|
||||||
|
}
|
||||||
|
Keys.onEscapePressed: function (event) {
|
||||||
|
modal.hide()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
Qt.callLater(function () {
|
||||||
|
forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: modal
|
||||||
|
function onOpened() {
|
||||||
|
Qt.callLater(function () {
|
||||||
|
searchField.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Container
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - ClipboardConstants.headerHeight - 70
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: clipboardListView
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
model: filteredModel
|
||||||
|
|
||||||
|
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
interactive: true
|
||||||
|
flickDeceleration: 1500
|
||||||
|
maximumFlickVelocity: 2000
|
||||||
|
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||||
|
boundsMovement: Flickable.FollowBoundsBehavior
|
||||||
|
pressDelay: 0
|
||||||
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const itemHeight = ClipboardConstants.itemHeight + spacing
|
||||||
|
const itemY = index * itemHeight
|
||||||
|
const itemBottom = itemY + itemHeight
|
||||||
|
if (itemY < contentY) {
|
||||||
|
contentY = itemY
|
||||||
|
} else if (itemBottom > contentY + height) {
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) {
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "No clipboard entries found"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: filteredModel.count === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ClipboardEntry {
|
||||||
|
required property int index
|
||||||
|
required property var model
|
||||||
|
|
||||||
|
width: clipboardListView.width
|
||||||
|
height: ClipboardConstants.itemHeight
|
||||||
|
entryData: model.entry
|
||||||
|
entryIndex: index + 1
|
||||||
|
itemIndex: index
|
||||||
|
isSelected: clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
|
||||||
|
modal: clipboardContent.modal
|
||||||
|
listView: clipboardListView
|
||||||
|
onCopyRequested: clipboardContent.modal.copyEntry(model.entry)
|
||||||
|
onDeleteRequested: clipboardContent.modal.deleteEntry(model.entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer for keyboard hints
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard Hints Overlay
|
||||||
|
ClipboardKeyboardHints {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
visible: modal.showKeyboardHints
|
||||||
|
}
|
||||||
|
}
|
||||||
137
Modals/Clipboard/ClipboardEntry.qml
Normal file
137
Modals/Clipboard/ClipboardEntry.qml
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: entry
|
||||||
|
|
||||||
|
required property string entryData
|
||||||
|
required property int entryIndex
|
||||||
|
required property int itemIndex
|
||||||
|
required property bool isSelected
|
||||||
|
required property var modal
|
||||||
|
required property var listView
|
||||||
|
|
||||||
|
signal copyRequested
|
||||||
|
signal deleteRequested
|
||||||
|
|
||||||
|
readonly property string entryType: modal ? modal.getEntryType(entryData) : "text"
|
||||||
|
readonly property string entryPreview: modal ? modal.getEntryPreview(entryData) : entryData
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (isSelected) {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
|
||||||
|
}
|
||||||
|
return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
|
||||||
|
}
|
||||||
|
border.color: {
|
||||||
|
if (isSelected) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5)
|
||||||
|
}
|
||||||
|
return Theme.outlineStrong
|
||||||
|
}
|
||||||
|
border.width: isSelected ? 1.5 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
// Index indicator
|
||||||
|
Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
color: Theme.primarySelected
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: entryIndex.toString()
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content area
|
||||||
|
Row {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - 68
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
// Thumbnail/Icon
|
||||||
|
ClipboardThumbnail {
|
||||||
|
width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
|
||||||
|
height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
entryData: entry.entryData
|
||||||
|
entryType: entry.entryType
|
||||||
|
modal: entry.modal
|
||||||
|
listView: entry.listView
|
||||||
|
itemIndex: entry.itemIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text content
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize) - Theme.spacingM
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
switch (entryType) {
|
||||||
|
case "image":
|
||||||
|
return "Image • " + entryPreview
|
||||||
|
case "long_text":
|
||||||
|
return "Long Text"
|
||||||
|
default:
|
||||||
|
return "Text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: entryPreview
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
maximumLineCount: entryType === "long_text" ? 3 : 1
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
DankActionButton {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 6
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: deleteRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click area
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: 40
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: copyRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
65
Modals/Clipboard/ClipboardHeader.qml
Normal file
65
Modals/Clipboard/ClipboardHeader.qml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: header
|
||||||
|
|
||||||
|
property int totalCount: 0
|
||||||
|
property bool showKeyboardHints: false
|
||||||
|
|
||||||
|
signal keyboardHintsToggled
|
||||||
|
signal clearAllClicked
|
||||||
|
signal closeClicked
|
||||||
|
|
||||||
|
height: ClipboardConstants.headerHeight
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "content_paste"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: `Clipboard History (${totalCount})`
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "info"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
||||||
|
onClicked: keyboardHintsToggled()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "delete_sweep"
|
||||||
|
iconSize: Theme.iconSize
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: clearAllClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: closeClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
216
Modals/Clipboard/ClipboardHistoryModal.qml
Normal file
216
Modals/Clipboard/ClipboardHistoryModal.qml
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: clipboardHistoryModal
|
||||||
|
|
||||||
|
property int totalCount: 0
|
||||||
|
property var clipboardEntries: []
|
||||||
|
property string searchText: ""
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
property bool showKeyboardHints: false
|
||||||
|
property Component clipboardContent
|
||||||
|
property int activeImageLoads: 0
|
||||||
|
readonly property int maxConcurrentLoads: 3
|
||||||
|
|
||||||
|
function updateFilteredModel() {
|
||||||
|
filteredClipboardModel.clear()
|
||||||
|
for (var i = 0; i < clipboardModel.count; i++) {
|
||||||
|
const entry = clipboardModel.get(i).entry
|
||||||
|
if (searchText.trim().length === 0) {
|
||||||
|
filteredClipboardModel.append({
|
||||||
|
"entry": entry
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const content = getEntryPreview(entry).toLowerCase()
|
||||||
|
if (content.includes(searchText.toLowerCase())) {
|
||||||
|
filteredClipboardModel.append({
|
||||||
|
"entry": entry
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
||||||
|
if (filteredClipboardModel.count === 0) {
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex >= filteredClipboardModel.count) {
|
||||||
|
selectedIndex = filteredClipboardModel.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open()
|
||||||
|
clipboardHistoryModal.searchText = ""
|
||||||
|
clipboardHistoryModal.activeImageLoads = 0
|
||||||
|
refreshClipboard()
|
||||||
|
keyboardController.reset()
|
||||||
|
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.text = ""
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close()
|
||||||
|
clipboardHistoryModal.searchText = ""
|
||||||
|
clipboardHistoryModal.activeImageLoads = 0
|
||||||
|
updateFilteredModel()
|
||||||
|
keyboardController.reset()
|
||||||
|
cleanupTempFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupTempFiles() {
|
||||||
|
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshClipboard() {
|
||||||
|
clipboardProcesses.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyEntry(entry) {
|
||||||
|
const entryId = entry.split('\t')[0]
|
||||||
|
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
|
||||||
|
ToastService.showInfo("Copied to clipboard")
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEntry(entry) {
|
||||||
|
clipboardProcesses.deleteEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
clipboardProcesses.clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntryPreview(entry) {
|
||||||
|
let content = entry.replace(/^\s*\d+\s+/, "")
|
||||||
|
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
||||||
|
const dimensionMatch = content.match(/(\d+)x(\d+)/)
|
||||||
|
if (dimensionMatch) {
|
||||||
|
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
|
||||||
|
}
|
||||||
|
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
|
||||||
|
if (typeMatch) {
|
||||||
|
return `Image (${typeMatch[1].toUpperCase()})`
|
||||||
|
}
|
||||||
|
return "Image"
|
||||||
|
}
|
||||||
|
if (content.length > ClipboardConstants.previewLength) {
|
||||||
|
return content.substring(0, ClipboardConstants.previewLength) + "..."
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntryType(entry) {
|
||||||
|
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
if (entry.length > ClipboardConstants.longTextThreshold) {
|
||||||
|
return "long_text"
|
||||||
|
}
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
width: ClipboardConstants.modalWidth
|
||||||
|
height: ClipboardConstants.modalHeight
|
||||||
|
backgroundColor: Theme.popupBackground()
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
borderColor: Theme.outlineMedium
|
||||||
|
borderWidth: 1
|
||||||
|
enableShadow: true
|
||||||
|
onBackgroundClicked: hide()
|
||||||
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
|
keyboardController.handleKey(event)
|
||||||
|
}
|
||||||
|
content: clipboardContent
|
||||||
|
|
||||||
|
ClipboardKeyboardController {
|
||||||
|
id: keyboardController
|
||||||
|
modal: clipboardHistoryModal
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: clearConfirmDialog
|
||||||
|
confirmButtonText: "Clear All"
|
||||||
|
confirmButtonColor: Theme.primary
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
clipboardHistoryModal.shouldHaveFocus = false
|
||||||
|
} else if (clipboardHistoryModal.shouldBeVisible) {
|
||||||
|
clipboardHistoryModal.shouldHaveFocus = true
|
||||||
|
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
|
||||||
|
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
|
||||||
|
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property alias filteredClipboardModel: filteredClipboardModel
|
||||||
|
property alias clipboardModel: clipboardModel
|
||||||
|
property var confirmDialog: clearConfirmDialog
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: clipboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: filteredClipboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardProcesses {
|
||||||
|
id: clipboardProcesses
|
||||||
|
modal: clipboardHistoryModal
|
||||||
|
clipboardModel: clipboardModel
|
||||||
|
filteredClipboardModel: filteredClipboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
clipboardHistoryModal.show()
|
||||||
|
return "CLIPBOARD_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
clipboardHistoryModal.hide()
|
||||||
|
return "CLIPBOARD_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
clipboardHistoryModal.toggle()
|
||||||
|
return "CLIPBOARD_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "clipboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboardContent: Component {
|
||||||
|
ClipboardContent {
|
||||||
|
modal: clipboardHistoryModal
|
||||||
|
filteredModel: filteredClipboardModel
|
||||||
|
clearConfirmDialog: clipboardHistoryModal.confirmDialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
Modals/Clipboard/ClipboardKeyboardController.qml
Normal file
95
Modals/Clipboard/ClipboardKeyboardController.qml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: keyboardController
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
modal.selectedIndex = 0
|
||||||
|
modal.keyboardNavigationActive = false
|
||||||
|
modal.showKeyboardHints = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modal.keyboardNavigationActive = true
|
||||||
|
modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.filteredClipboardModel.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modal.keyboardNavigationActive = true
|
||||||
|
modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySelected() {
|
||||||
|
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
|
||||||
|
modal.copyEntry(selectedEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
|
||||||
|
modal.deleteEntry(selectedEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKey(event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (modal.keyboardNavigationActive) {
|
||||||
|
modal.keyboardNavigationActive = false
|
||||||
|
event.accepted = true
|
||||||
|
} else {
|
||||||
|
modal.hide()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Down) {
|
||||||
|
if (!modal.keyboardNavigationActive) {
|
||||||
|
modal.keyboardNavigationActive = true
|
||||||
|
modal.selectedIndex = 0
|
||||||
|
event.accepted = true
|
||||||
|
} else {
|
||||||
|
selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
if (!modal.keyboardNavigationActive) {
|
||||||
|
modal.keyboardNavigationActive = true
|
||||||
|
modal.selectedIndex = 0
|
||||||
|
event.accepted = true
|
||||||
|
} else if (modal.selectedIndex === 0) {
|
||||||
|
modal.keyboardNavigationActive = false
|
||||||
|
event.accepted = true
|
||||||
|
} else {
|
||||||
|
selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Delete && (event.modifiers & Qt.ShiftModifier)) {
|
||||||
|
modal.clearAll()
|
||||||
|
modal.hide()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (modal.keyboardNavigationActive) {
|
||||||
|
if ((event.key === Qt.Key_C && (event.modifiers & Qt.ControlModifier)) || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
copySelected()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Delete) {
|
||||||
|
deleteSelected()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === Qt.Key_F10) {
|
||||||
|
modal.showKeyboardHints = !modal.showKeyboardHints
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Modals/Clipboard/ClipboardKeyboardHints.qml
Normal file
42
Modals/Clipboard/ClipboardKeyboardHints.qml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: keyboardHints
|
||||||
|
|
||||||
|
height: ClipboardConstants.keyboardHintsHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 2
|
||||||
|
opacity: visible ? 1 : 0
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Shift+Del: Clear All • Esc: Close"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Modals/Clipboard/ClipboardProcesses.qml
Normal file
94
Modals/Clipboard/ClipboardProcesses.qml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: clipboardProcesses
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
required property var clipboardModel
|
||||||
|
required property var filteredClipboardModel
|
||||||
|
|
||||||
|
// Load clipboard entries
|
||||||
|
property var loadProcess: Process {
|
||||||
|
id: loadProcess
|
||||||
|
command: ["cliphist", "list"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
clipboardModel.clear()
|
||||||
|
const lines = text.trim().split('\n')
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().length > 0) {
|
||||||
|
clipboardModel.append({
|
||||||
|
"entry": line
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modal.updateFilteredModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete single entry
|
||||||
|
property var deleteProcess: Process {
|
||||||
|
id: deleteProcess
|
||||||
|
property string deletedEntry: ""
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
for (var i = 0; i < clipboardModel.count; i++) {
|
||||||
|
if (clipboardModel.get(i).entry === deleteProcess.deletedEntry) {
|
||||||
|
clipboardModel.remove(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var j = 0; j < filteredClipboardModel.count; j++) {
|
||||||
|
if (filteredClipboardModel.get(j).entry === deleteProcess.deletedEntry) {
|
||||||
|
filteredClipboardModel.remove(j)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modal.totalCount = filteredClipboardModel.count
|
||||||
|
if (filteredClipboardModel.count === 0) {
|
||||||
|
modal.keyboardNavigationActive = false
|
||||||
|
modal.selectedIndex = 0
|
||||||
|
} else if (modal.selectedIndex >= filteredClipboardModel.count) {
|
||||||
|
modal.selectedIndex = filteredClipboardModel.count - 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Failed to delete clipboard entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all entries
|
||||||
|
property var clearProcess: Process {
|
||||||
|
id: clearProcess
|
||||||
|
command: ["cliphist", "wipe"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
clipboardModel.clear()
|
||||||
|
filteredClipboardModel.clear()
|
||||||
|
modal.totalCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
loadProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEntry(entry) {
|
||||||
|
deleteProcess.deletedEntry = entry
|
||||||
|
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`]
|
||||||
|
deleteProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
clearProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Modals/Clipboard/ClipboardThumbnail.qml
Normal file
174
Modals/Clipboard/ClipboardThumbnail.qml
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: thumbnail
|
||||||
|
|
||||||
|
required property string entryData
|
||||||
|
required property string entryType
|
||||||
|
required property var modal
|
||||||
|
required property var listView
|
||||||
|
required property int itemIndex
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: thumbnailImage
|
||||||
|
|
||||||
|
property string entryId: entryData.split('\t')[0]
|
||||||
|
property bool isVisible: false
|
||||||
|
property string cachedImageData: ""
|
||||||
|
property bool loadQueued: false
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
cache: false
|
||||||
|
visible: false
|
||||||
|
asynchronous: true
|
||||||
|
sourceSize.width: 128
|
||||||
|
sourceSize.height: 128
|
||||||
|
|
||||||
|
onCachedImageDataChanged: {
|
||||||
|
if (cachedImageData) {
|
||||||
|
source = ""
|
||||||
|
source = `data:image/png;base64,${cachedImageData}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryLoadImage() {
|
||||||
|
if (!loadQueued && entryType === "image" && !cachedImageData) {
|
||||||
|
loadQueued = true
|
||||||
|
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||||
|
modal.activeImageLoads++
|
||||||
|
imageLoader.running = true
|
||||||
|
} else {
|
||||||
|
retryTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: retryTimer
|
||||||
|
interval: ClipboardConstants.retryInterval
|
||||||
|
onTriggered: {
|
||||||
|
if (thumbnailImage.loadQueued && !imageLoader.running) {
|
||||||
|
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||||
|
modal.activeImageLoads++
|
||||||
|
imageLoader.running = true
|
||||||
|
} else {
|
||||||
|
retryTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (entryType !== "image") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if item is visible on screen initially
|
||||||
|
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing)
|
||||||
|
const viewTop = listView.contentY
|
||||||
|
const viewBottom = viewTop + listView.height
|
||||||
|
isVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom)
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
tryLoadImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: listView
|
||||||
|
function onContentYChanged() {
|
||||||
|
if (entryType !== "image") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing)
|
||||||
|
const viewTop = listView.contentY - ClipboardConstants.viewportBuffer
|
||||||
|
const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer
|
||||||
|
const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom)
|
||||||
|
|
||||||
|
if (nowVisible && !thumbnailImage.isVisible) {
|
||||||
|
thumbnailImage.isVisible = true
|
||||||
|
thumbnailImage.tryLoadImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: imageLoader
|
||||||
|
running: false
|
||||||
|
command: ["sh", "-c", `cliphist decode ${thumbnailImage.entryId} | base64 -w 0`]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const imageData = text.trim()
|
||||||
|
if (imageData && imageData.length > 0) {
|
||||||
|
thumbnailImage.cachedImageData = imageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
thumbnailImage.loadQueued = false
|
||||||
|
if (modal.activeImageLoads > 0) {
|
||||||
|
modal.activeImageLoads--
|
||||||
|
}
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("Failed to load clipboard image:", thumbnailImage.entryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounded mask effect for images
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
source: thumbnailImage
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: clipboardCircularMask
|
||||||
|
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: clipboardCircularMask
|
||||||
|
width: ClipboardConstants.thumbnailSize - 4
|
||||||
|
height: ClipboardConstants.thumbnailSize - 4
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "black"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback icon
|
||||||
|
DankIcon {
|
||||||
|
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
||||||
|
name: {
|
||||||
|
if (entryType === "image") {
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
if (entryType === "long_text") {
|
||||||
|
return "subject"
|
||||||
|
}
|
||||||
|
return "content_copy"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,904 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: clipboardHistoryModal
|
|
||||||
|
|
||||||
property int totalCount: 0
|
|
||||||
property var activeTheme: Theme
|
|
||||||
property bool showClearConfirmation: false
|
|
||||||
property var clipboardEntries: []
|
|
||||||
property string searchText: ""
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property bool showKeyboardHints: false
|
|
||||||
property Component clipboardContent
|
|
||||||
|
|
||||||
function updateFilteredModel() {
|
|
||||||
filteredClipboardModel.clear()
|
|
||||||
for (var i = 0; i < clipboardModel.count; i++) {
|
|
||||||
const entry = clipboardModel.get(i).entry
|
|
||||||
if (searchText.trim().length === 0) {
|
|
||||||
filteredClipboardModel.append({
|
|
||||||
"entry": entry
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const content = getEntryPreview(entry).toLowerCase()
|
|
||||||
if (content.includes(searchText.toLowerCase()))
|
|
||||||
filteredClipboardModel.append({
|
|
||||||
"entry": entry
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
|
||||||
// Clamp selectedIndex to valid range
|
|
||||||
if (filteredClipboardModel.count === 0) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
|
||||||
selectedIndex = filteredClipboardModel.count - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible)
|
|
||||||
hide()
|
|
||||||
else
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
open()
|
|
||||||
clipboardHistoryModal.searchText = ""
|
|
||||||
|
|
||||||
initializeThumbnailSystem()
|
|
||||||
refreshClipboard()
|
|
||||||
keyboardController.reset()
|
|
||||||
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item && contentLoader.item.searchField) {
|
|
||||||
contentLoader.item.searchField.text = ""
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close()
|
|
||||||
clipboardHistoryModal.searchText = ""
|
|
||||||
|
|
||||||
updateFilteredModel()
|
|
||||||
keyboardController.reset()
|
|
||||||
cleanupTempFiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeThumbnailSystem() {}
|
|
||||||
|
|
||||||
function cleanupTempFiles() {
|
|
||||||
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateThumbnails() {}
|
|
||||||
|
|
||||||
function refreshClipboard() {
|
|
||||||
clipboardProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyEntry(entry) {
|
|
||||||
const entryId = entry.split('\t')[0]
|
|
||||||
Quickshell.execDetached(
|
|
||||||
["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
|
|
||||||
ToastService.showInfo("Copied to clipboard")
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteEntry(entry) {
|
|
||||||
deleteProcess.deletedEntry = entry
|
|
||||||
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(
|
|
||||||
/'/g, "'\\''")}' | cliphist delete`]
|
|
||||||
deleteProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAll() {
|
|
||||||
clearProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntryPreview(entry) {
|
|
||||||
let content = entry.replace(/^\s*\d+\s+/, "")
|
|
||||||
if (content.includes("image/") || content.includes("binary data")
|
|
||||||
|| /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
|
||||||
const dimensionMatch = content.match(/(\d+)x(\d+)/)
|
|
||||||
if (dimensionMatch)
|
|
||||||
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
|
|
||||||
|
|
||||||
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
|
|
||||||
if (typeMatch)
|
|
||||||
return `Image (${typeMatch[1].toUpperCase()})`
|
|
||||||
|
|
||||||
return "Image"
|
|
||||||
}
|
|
||||||
if (content.length > 100)
|
|
||||||
return content.substring(0, 100) + "..."
|
|
||||||
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntryType(entry) {
|
|
||||||
if (entry.includes("image/") || entry.includes("binary data")
|
|
||||||
|| /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry)
|
|
||||||
|| /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry))
|
|
||||||
return "image"
|
|
||||||
|
|
||||||
if (entry.length > 200)
|
|
||||||
return "long_text"
|
|
||||||
|
|
||||||
return "text"
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 650
|
|
||||||
height: 550
|
|
||||||
backgroundColor: Theme.popupBackground()
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
borderColor: Theme.outlineMedium
|
|
||||||
borderWidth: 1
|
|
||||||
enableShadow: true
|
|
||||||
onBackgroundClicked: {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
|
||||||
keyboardController.handleKey(event)
|
|
||||||
}
|
|
||||||
content: clipboardContent
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: keyboardController
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
selectedIndex = 0
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
showKeyboardHints = false
|
|
||||||
if (typeof clipboardListView !== 'undefined' && clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (filteredClipboardModel.count === 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1,
|
|
||||||
filteredClipboardModel.count - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (filteredClipboardModel.count === 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function copySelected() {
|
|
||||||
if (filteredClipboardModel.count === 0 || selectedIndex < 0
|
|
||||||
|| selectedIndex >= filteredClipboardModel.count)
|
|
||||||
return
|
|
||||||
|
|
||||||
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
|
|
||||||
copyEntry(selectedEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteSelected() {
|
|
||||||
if (filteredClipboardModel.count === 0 || selectedIndex < 0
|
|
||||||
|| selectedIndex >= filteredClipboardModel.count)
|
|
||||||
return
|
|
||||||
|
|
||||||
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
|
|
||||||
deleteEntry(selectedEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKey(event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
if (keyboardNavigationActive) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
if (typeof clipboardListView !== 'undefined'
|
|
||||||
&& clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = false
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
if (!keyboardNavigationActive) {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = 0
|
|
||||||
if (typeof clipboardListView !== 'undefined'
|
|
||||||
&& clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = true
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
if (!keyboardNavigationActive) {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = 0
|
|
||||||
if (typeof clipboardListView !== 'undefined'
|
|
||||||
&& clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = true
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else if (selectedIndex === 0) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
if (typeof clipboardListView !== 'undefined'
|
|
||||||
&& clipboardListView)
|
|
||||||
clipboardListView.keyboardActive = false
|
|
||||||
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Delete
|
|
||||||
&& (event.modifiers & Qt.ShiftModifier)) {
|
|
||||||
clearAll()
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (keyboardNavigationActive) {
|
|
||||||
if ((event.key === Qt.Key_C
|
|
||||||
&& (event.modifiers & Qt.ControlModifier))
|
|
||||||
|| event.key === Qt.Key_Return
|
|
||||||
|| event.key === Qt.Key_Enter) {
|
|
||||||
copySelected()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Delete) {
|
|
||||||
deleteSelected()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.key === Qt.Key_F10) {
|
|
||||||
showKeyboardHints = !showKeyboardHints
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: clearConfirmDialog
|
|
||||||
|
|
||||||
visible: showClearConfirmation
|
|
||||||
width: 350
|
|
||||||
height: 150
|
|
||||||
onBackgroundClicked: {
|
|
||||||
showClearConfirmation = false
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Clear All History?"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "This will permanently delete all clipboard history."
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: cancelClearButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Cancel"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cancelClearButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: showClearConfirmation = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: confirmClearButton.containsMouse ? Theme.errorPressed : Theme.error
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Clear All"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primaryText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: confirmClearButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
clearAll()
|
|
||||||
showClearConfirmation = false
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: clipboardModel
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: filteredClipboardModel
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: clipboardProcess
|
|
||||||
|
|
||||||
command: ["cliphist", "list"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
clipboardModel.clear()
|
|
||||||
const lines = text.trim().split('\n')
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim().length > 0)
|
|
||||||
clipboardModel.append({
|
|
||||||
"entry": line
|
|
||||||
})
|
|
||||||
}
|
|
||||||
updateFilteredModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: deleteProcess
|
|
||||||
|
|
||||||
property string deletedEntry: ""
|
|
||||||
|
|
||||||
running: false
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
// Just remove the item from models instead of re-fetching everything
|
|
||||||
for (var i = 0; i < clipboardModel.count; i++) {
|
|
||||||
if (clipboardModel.get(
|
|
||||||
i).entry === deleteProcess.deletedEntry) {
|
|
||||||
clipboardModel.remove(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var j = 0; j < filteredClipboardModel.count; j++) {
|
|
||||||
if (filteredClipboardModel.get(
|
|
||||||
j).entry === deleteProcess.deletedEntry) {
|
|
||||||
filteredClipboardModel.remove(j)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
|
||||||
// Clamp selectedIndex to valid range
|
|
||||||
if (filteredClipboardModel.count === 0) {
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
|
||||||
selectedIndex = filteredClipboardModel.count - 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Failed to delete clipboard entry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: clearProcess
|
|
||||||
|
|
||||||
command: ["cliphist", "wipe"]
|
|
||||||
running: false
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
clipboardModel.clear()
|
|
||||||
filteredClipboardModel.clear()
|
|
||||||
totalCount = 0
|
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open() {
|
|
||||||
clipboardHistoryModal.show()
|
|
||||||
return "CLIPBOARD_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
hide()
|
|
||||||
return "CLIPBOARD_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
clipboardHistoryModal.toggle()
|
|
||||||
return "CLIPBOARD_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "clipboard"
|
|
||||||
}
|
|
||||||
|
|
||||||
clipboardContent: Component {
|
|
||||||
Item {
|
|
||||||
id: clipboardContent
|
|
||||||
property alias searchField: searchField
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
focus: false
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "content_paste"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: `Clipboard History (${totalCount})`
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "info"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
|
||||||
hoverColor: Theme.primaryHover
|
|
||||||
onClicked: {
|
|
||||||
showKeyboardHints = !showKeyboardHints
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "delete_sweep"
|
|
||||||
iconSize: Theme.iconSize
|
|
||||||
iconColor: Theme.error
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: {
|
|
||||||
showClearConfirmation = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: ""
|
|
||||||
leftIconName: "search"
|
|
||||||
showClearButton: true
|
|
||||||
focus: true
|
|
||||||
ignoreLeftRightKeys: true
|
|
||||||
keyForwardTargets: [modalFocusScope]
|
|
||||||
onTextChanged: {
|
|
||||||
clipboardHistoryModal.searchText = text
|
|
||||||
updateFilteredModel()
|
|
||||||
}
|
|
||||||
Keys.onEscapePressed: function (event) {
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: clipboardHistoryModal
|
|
||||||
function onOpened() {
|
|
||||||
Qt.callLater(function () {
|
|
||||||
searchField.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 110
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: clipboardListView
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
var itemHeight = 72 + spacing
|
|
||||||
var itemY = index * itemHeight
|
|
||||||
var itemBottom = itemY + itemHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
clip: true
|
|
||||||
model: filteredClipboardModel
|
|
||||||
currentIndex: selectedIndex
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
interactive: true
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
|
||||||
pressDelay: 0
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive && currentIndex >= 0)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No clipboard entries found"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: filteredClipboardModel.count === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
property string entryType: getEntryType(model.entry)
|
|
||||||
property string entryPreview: getEntryPreview(
|
|
||||||
model.entry)
|
|
||||||
property int entryIndex: index + 1
|
|
||||||
property string entryData: model.entry
|
|
||||||
property alias thumbnailImageSource: thumbnailImageSource
|
|
||||||
|
|
||||||
width: clipboardListView.width
|
|
||||||
height: 72
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (keyboardNavigationActive
|
|
||||||
&& index === selectedIndex)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.2)
|
|
||||||
|
|
||||||
return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
if (keyboardNavigationActive
|
|
||||||
&& index === selectedIndex)
|
|
||||||
return Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.5)
|
|
||||||
|
|
||||||
return Theme.outlineStrong
|
|
||||||
}
|
|
||||||
border.width: keyboardNavigationActive
|
|
||||||
&& index === selectedIndex ? 1.5 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
radius: 12
|
|
||||||
color: Theme.primarySelected
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: entryIndex.toString()
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - 68
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: entryType === "image" ? 48 : Theme.iconSize
|
|
||||||
height: entryType === "image" ? 48 : Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
CachingImage {
|
|
||||||
id: thumbnailImageSource
|
|
||||||
|
|
||||||
property string entryId: model.entry.split(
|
|
||||||
'\t')[0]
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: entryType === "image"
|
|
||||||
&& imageLoader.imageData ? `data:image/png;base64,${imageLoader.imageData}` : ""
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
smooth: true
|
|
||||||
cache: true
|
|
||||||
visible: false
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: imageLoader
|
|
||||||
|
|
||||||
property string imageData: ""
|
|
||||||
|
|
||||||
command: ["sh", "-c", `cliphist decode ${thumbnailImageSource.entryId} | base64 -w 0`]
|
|
||||||
running: entryType === "image"
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
imageLoader.imageData = text.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
source: thumbnailImageSource
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: clipboardCircularMask
|
|
||||||
visible: entryType === "image"
|
|
||||||
&& thumbnailImageSource.status === Image.Ready
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: clipboardCircularMask
|
|
||||||
|
|
||||||
width: 48 - 4
|
|
||||||
height: 48 - 4
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
visible: !(entryType === "image"
|
|
||||||
&& thumbnailImageSource.status
|
|
||||||
=== Image.Ready)
|
|
||||||
name: {
|
|
||||||
if (entryType === "image")
|
|
||||||
return "image"
|
|
||||||
|
|
||||||
if (entryType === "long_text")
|
|
||||||
return "subject"
|
|
||||||
|
|
||||||
return "content_copy"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - (entryType === "image" ? 48 : Theme.iconSize) - Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
switch (entryType) {
|
|
||||||
case "image":
|
|
||||||
return "Image • " + entryPreview
|
|
||||||
case "long_text":
|
|
||||||
return "Long Text"
|
|
||||||
default:
|
|
||||||
return "Text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: contentText
|
|
||||||
|
|
||||||
text: entryPreview
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
maximumLineCount: entryType === "long_text" ? 3 : 1
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 6
|
|
||||||
iconColor: Theme.error
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: {
|
|
||||||
deleteEntry(model.entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 40
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: copyEntry(model.entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: showKeyboardHints ? 80 + Theme.spacingL : 0
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
|
||||||
Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.95)
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 2
|
|
||||||
opacity: showKeyboardHints ? 1 : 0
|
|
||||||
z: 100
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Shift+Del: Clear All • Esc: Close"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
Modals/ColorPickerModal.qml
Normal file
37
Modals/ColorPickerModal.qml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Qt.labs.platform
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: colorPickerModal
|
||||||
|
|
||||||
|
signal colorSelected(color selectedColor)
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
colorDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
colorDialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyColorToClipboard(colorValue) {
|
||||||
|
Quickshell.execDetached(["sh", "-c", `echo "${colorValue}" | wl-copy`])
|
||||||
|
ToastService.showInfo(`Color ${colorValue} copied to clipboard`)
|
||||||
|
console.log("Copied color to clipboard:", colorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorDialog {
|
||||||
|
id: colorDialog
|
||||||
|
title: "Color Picker - Select and copy color"
|
||||||
|
color: Theme.primary
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
const colorString = color.toString()
|
||||||
|
copyColorToClipboard(colorString)
|
||||||
|
colorSelected(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
227
Modals/Common/ConfirmModal.qml
Normal file
227
Modals/Common/ConfirmModal.qml
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string confirmTitle: ""
|
||||||
|
property string confirmMessage: ""
|
||||||
|
property string confirmButtonText: "Confirm"
|
||||||
|
property string cancelButtonText: "Cancel"
|
||||||
|
property color confirmButtonColor: Theme.primary
|
||||||
|
property var onConfirm: function () {}
|
||||||
|
property var onCancel: function () {}
|
||||||
|
property int selectedButton: -1
|
||||||
|
property bool keyboardNavigation: false
|
||||||
|
|
||||||
|
function show(title, message, onConfirmCallback, onCancelCallback) {
|
||||||
|
confirmTitle = title || ""
|
||||||
|
confirmMessage = message || ""
|
||||||
|
confirmButtonText = "Confirm"
|
||||||
|
cancelButtonText = "Cancel"
|
||||||
|
confirmButtonColor = Theme.primary
|
||||||
|
onConfirm = onConfirmCallback || (() => {})
|
||||||
|
onCancel = onCancelCallback || (() => {})
|
||||||
|
selectedButton = -1
|
||||||
|
keyboardNavigation = false
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWithOptions(options) {
|
||||||
|
confirmTitle = options.title || ""
|
||||||
|
confirmMessage = options.message || ""
|
||||||
|
confirmButtonText = options.confirmText || "Confirm"
|
||||||
|
cancelButtonText = options.cancelText || "Cancel"
|
||||||
|
confirmButtonColor = options.confirmColor || Theme.primary
|
||||||
|
onConfirm = options.onConfirm || (() => {})
|
||||||
|
onCancel = options.onCancel || (() => {})
|
||||||
|
selectedButton = -1
|
||||||
|
keyboardNavigation = false
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectButton() {
|
||||||
|
close()
|
||||||
|
if (selectedButton === 0) {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (onConfirm) {
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
allowStacking: true
|
||||||
|
width: 350
|
||||||
|
height: 160
|
||||||
|
enableShadow: true
|
||||||
|
shouldHaveFocus: true
|
||||||
|
onBackgroundClicked: {
|
||||||
|
close()
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onOpened: {
|
||||||
|
modalFocusScope.forceActiveFocus()
|
||||||
|
modalFocusScope.focus = true
|
||||||
|
shouldHaveFocus = true
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
close()
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Left:
|
||||||
|
case Qt.Key_Up:
|
||||||
|
keyboardNavigation = true
|
||||||
|
selectedButton = 0
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Right:
|
||||||
|
case Qt.Key_Down:
|
||||||
|
keyboardNavigation = true
|
||||||
|
selectedButton = 1
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
keyboardNavigation = true
|
||||||
|
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
if (selectedButton !== -1) {
|
||||||
|
selectButton()
|
||||||
|
} else {
|
||||||
|
selectedButton = 1
|
||||||
|
selectButton()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: confirmTitle
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: confirmMessage
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (keyboardNavigation && selectedButton === 0) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
|
} else if (cancelButton.containsMouse) {
|
||||||
|
return Theme.surfacePressed
|
||||||
|
} else {
|
||||||
|
return Theme.surfaceVariantAlpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
|
||||||
|
border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: cancelButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
selectedButton = 0
|
||||||
|
selectButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
const baseColor = confirmButtonColor
|
||||||
|
if (keyboardNavigation && selectedButton === 1) {
|
||||||
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
|
||||||
|
} else if (confirmButton.containsMouse) {
|
||||||
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
|
||||||
|
} else {
|
||||||
|
return baseColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
|
||||||
|
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: confirmButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.primaryText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: confirmButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
selectedButton = 1
|
||||||
|
selectButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import qs.Common
|
|||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "quickshell:modal"
|
||||||
|
|
||||||
property alias content: contentLoader.sourceComponent
|
property alias content: contentLoader.sourceComponent
|
||||||
property alias contentLoader: contentLoader
|
property alias contentLoader: contentLoader
|
||||||
property real width: 400
|
property real width: 400
|
||||||
@@ -20,14 +22,13 @@ PanelWindow {
|
|||||||
property bool closeOnEscapeKey: true
|
property bool closeOnEscapeKey: true
|
||||||
property bool closeOnBackgroundClick: true
|
property bool closeOnBackgroundClick: true
|
||||||
property string animationType: "scale"
|
property string animationType: "scale"
|
||||||
property int animationDuration: Theme.mediumDuration
|
property int animationDuration: Theme.shorterDuration
|
||||||
property var animationEasing: Theme.emphasizedEasing
|
property var animationEasing: Theme.emphasizedEasing
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
property color backgroundColor: Theme.surfaceContainer
|
||||||
property color borderColor: Theme.outlineMedium
|
property color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 1
|
property real borderWidth: 1
|
||||||
property real cornerRadius: Theme.cornerRadius
|
property real cornerRadius: Theme.cornerRadius
|
||||||
property bool enableShadow: false
|
property bool enableShadow: false
|
||||||
// Expose the focusScope for external access
|
|
||||||
property alias modalFocusScope: focusScope
|
property alias modalFocusScope: focusScope
|
||||||
property bool shouldBeVisible: false
|
property bool shouldBeVisible: false
|
||||||
property bool shouldHaveFocus: shouldBeVisible
|
property bool shouldHaveFocus: shouldBeVisible
|
||||||
@@ -38,15 +39,6 @@ PanelWindow {
|
|||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
signal backgroundClicked
|
signal backgroundClicked
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: ModalManager
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
ModalManager.openModal(root)
|
ModalManager.openModal(root)
|
||||||
closeTimer.stop()
|
closeTimer.stop()
|
||||||
@@ -61,15 +53,16 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (shouldBeVisible)
|
if (shouldBeVisible) {
|
||||||
close()
|
close()
|
||||||
else
|
} else {
|
||||||
open()
|
open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: shouldBeVisible
|
visible: shouldBeVisible
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under modal
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
@@ -84,6 +77,16 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCloseAllModalsExcept(excludedModal) {
|
||||||
|
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: ModalManager
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: closeTimer
|
id: closeTimer
|
||||||
|
|
||||||
@@ -112,13 +115,10 @@ PanelWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: root.closeOnBackgroundClick
|
enabled: root.closeOnBackgroundClick
|
||||||
onClicked: mouse => {
|
onClicked: mouse => {
|
||||||
var localPos = mapToItem(contentContainer,
|
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
||||||
mouse.x, mouse.y)
|
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
||||||
if (localPos.x < 0
|
root.backgroundClicked()
|
||||||
|| localPos.x > contentContainer.width
|
}
|
||||||
|| localPos.y < 0
|
|
||||||
|| localPos.y > contentContainer.height)
|
|
||||||
root.backgroundClicked()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,19 +137,20 @@ PanelWindow {
|
|||||||
height: root.height
|
height: root.height
|
||||||
anchors.centerIn: positioning === "center" ? parent : undefined
|
anchors.centerIn: positioning === "center" ? parent : undefined
|
||||||
x: {
|
x: {
|
||||||
if (positioning === "top-right")
|
if (positioning === "top-right") {
|
||||||
return Math.max(Theme.spacingL,
|
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL)
|
||||||
root.screenWidth - width - Theme.spacingL)
|
} else if (positioning === "custom") {
|
||||||
else if (positioning === "custom")
|
|
||||||
return root.customPosition.x
|
return root.customPosition.x
|
||||||
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
y: {
|
y: {
|
||||||
if (positioning === "top-right")
|
if (positioning === "top-right") {
|
||||||
return Theme.barHeight + Theme.spacingXS
|
return Theme.barHeight + Theme.spacingXS
|
||||||
else if (positioning === "custom")
|
} else if (positioning === "custom") {
|
||||||
return root.customPosition.y
|
return root.customPosition.y
|
||||||
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
color: root.backgroundColor
|
color: root.backgroundColor
|
||||||
radius: root.cornerRadius
|
radius: root.cornerRadius
|
||||||
@@ -157,12 +158,7 @@ PanelWindow {
|
|||||||
border.width: root.borderWidth
|
border.width: root.borderWidth
|
||||||
layer.enabled: root.enableShadow
|
layer.enabled: root.enableShadow
|
||||||
opacity: root.shouldBeVisible ? 1 : 0
|
opacity: root.shouldBeVisible ? 1 : 0
|
||||||
scale: {
|
scale: root.animationType === "scale" ? (root.shouldBeVisible ? 1 : 0.9) : 1
|
||||||
if (root.animationType === "scale")
|
|
||||||
return root.shouldBeVisible ? 1 : 0.9
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
transform: root.animationType === "slide" ? slideTransform : null
|
transform: root.animationType === "slide" ? slideTransform : null
|
||||||
|
|
||||||
Translate {
|
Translate {
|
||||||
@@ -214,34 +210,25 @@ PanelWindow {
|
|||||||
visible: root.visible // Only active when the modal is visible
|
visible: root.visible // Only active when the modal is visible
|
||||||
focus: root.visible
|
focus: root.visible
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
console.log(
|
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||||
"DankModal escape pressed - shouldHaveFocus:",
|
|
||||||
shouldHaveFocus, "closeOnEscapeKey:",
|
|
||||||
root.closeOnEscapeKey, "objectName:",
|
|
||||||
root.objectName || "unnamed")
|
|
||||||
if (root.closeOnEscapeKey
|
|
||||||
&& shouldHaveFocus) {
|
|
||||||
console.log("DankModal handling escape")
|
|
||||||
root.close()
|
root.close()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible && shouldHaveFocus)
|
if (visible && shouldHaveFocus) {
|
||||||
Qt.callLater(function () {
|
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||||
focusScope.forceActiveFocus()
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
|
||||||
function onShouldHaveFocusChanged() {
|
function onShouldHaveFocusChanged() {
|
||||||
if (shouldHaveFocus && visible) {
|
if (shouldHaveFocus && visible) {
|
||||||
Qt.callLater(function () {
|
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||||
focusScope.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
908
Modals/FileBrowser/FileBrowserModal.qml
Normal file
908
Modals/FileBrowser/FileBrowserModal.qml
Normal file
@@ -0,0 +1,908 @@
|
|||||||
|
import Qt.labs.folderlistmodel
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: fileBrowserModal
|
||||||
|
|
||||||
|
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||||
|
property string currentPath: ""
|
||||||
|
property var fileExtensions: ["*.*"]
|
||||||
|
property alias filterExtensions: fileBrowserModal.fileExtensions
|
||||||
|
property string browserTitle: "Select File"
|
||||||
|
property string browserIcon: "folder_open"
|
||||||
|
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
|
||||||
|
property bool showHiddenFiles: false
|
||||||
|
property int selectedIndex: -1
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
property bool backButtonFocused: false
|
||||||
|
property bool saveMode: false // Enable save functionality
|
||||||
|
property string defaultFileName: "" // Default filename for save mode
|
||||||
|
property int keyboardSelectionIndex: -1
|
||||||
|
property bool keyboardSelectionRequested: false
|
||||||
|
property bool showKeyboardHints: false
|
||||||
|
property bool showFileInfo: false
|
||||||
|
property string selectedFilePath: ""
|
||||||
|
property string selectedFileName: ""
|
||||||
|
property bool selectedFileIsDir: false
|
||||||
|
property bool showOverwriteConfirmation: false
|
||||||
|
property string pendingFilePath: ""
|
||||||
|
property bool weAvailable: false
|
||||||
|
property string wePath: ""
|
||||||
|
property bool weMode: false
|
||||||
|
|
||||||
|
signal fileSelected(string path)
|
||||||
|
|
||||||
|
function isImageFile(fileName) {
|
||||||
|
if (!fileName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const ext = fileName.toLowerCase().split('.').pop()
|
||||||
|
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastPath() {
|
||||||
|
const lastPath = browserType === "wallpaper" ? SessionData.wallpaperLastPath : browserType === "profile" ? SessionData.profileLastPath : ""
|
||||||
|
return (lastPath && lastPath !== "") ? lastPath : homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLastPath(path) {
|
||||||
|
if (browserType === "wallpaper") {
|
||||||
|
SessionData.setWallpaperLastPath(path)
|
||||||
|
} else if (browserType === "profile") {
|
||||||
|
SessionData.setProfileLastPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedFileData(path, name, isDir) {
|
||||||
|
selectedFilePath = path
|
||||||
|
selectedFileName = name
|
||||||
|
selectedFileIsDir = isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateUp() {
|
||||||
|
const path = currentPath
|
||||||
|
if (path === homeDir)
|
||||||
|
return
|
||||||
|
|
||||||
|
const lastSlash = path.lastIndexOf('/')
|
||||||
|
if (lastSlash > 0) {
|
||||||
|
const newPath = path.substring(0, lastSlash)
|
||||||
|
if (newPath.length < homeDir.length) {
|
||||||
|
currentPath = homeDir
|
||||||
|
saveLastPath(homeDir)
|
||||||
|
} else {
|
||||||
|
currentPath = newPath
|
||||||
|
saveLastPath(newPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(path) {
|
||||||
|
currentPath = path
|
||||||
|
saveLastPath(path)
|
||||||
|
selectedIndex = -1
|
||||||
|
backButtonFocused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyboardFileSelection(index) {
|
||||||
|
if (index >= 0) {
|
||||||
|
keyboardSelectionTimer.targetIndex = index
|
||||||
|
keyboardSelectionTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeKeyboardSelection(index) {
|
||||||
|
keyboardSelectionIndex = index
|
||||||
|
keyboardSelectionRequested = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSaveFile(filePath) {
|
||||||
|
// Ensure the filePath has the correct file:// protocol format
|
||||||
|
var normalizedPath = filePath
|
||||||
|
if (!normalizedPath.startsWith("file://")) {
|
||||||
|
normalizedPath = "file://" + filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists by looking through the folder model
|
||||||
|
var exists = false
|
||||||
|
var fileName = filePath.split('/').pop()
|
||||||
|
|
||||||
|
for (var i = 0; i < folderModel.count; i++) {
|
||||||
|
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
pendingFilePath = normalizedPath
|
||||||
|
showOverwriteConfirmation = true
|
||||||
|
} else {
|
||||||
|
fileSelected(normalizedPath)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "fileBrowserModal"
|
||||||
|
allowStacking: true
|
||||||
|
Component.onCompleted: {
|
||||||
|
currentPath = getLastPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
property var steamPaths: [
|
||||||
|
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960",
|
||||||
|
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960",
|
||||||
|
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960",
|
||||||
|
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"
|
||||||
|
]
|
||||||
|
property int currentPathIndex: 0
|
||||||
|
|
||||||
|
function discoverWallpaperEngine() {
|
||||||
|
currentPathIndex = 0
|
||||||
|
checkNextPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNextPath() {
|
||||||
|
if (currentPathIndex >= steamPaths.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const wePath = steamPaths[currentPathIndex]
|
||||||
|
const cleanPath = wePath.replace(/^file:\/\//, '')
|
||||||
|
weDiscoveryProcess.command = ["test", "-d", cleanPath]
|
||||||
|
weDiscoveryProcess.wePath = wePath
|
||||||
|
weDiscoveryProcess.running = true
|
||||||
|
}
|
||||||
|
width: 800
|
||||||
|
height: 600
|
||||||
|
enableShadow: true
|
||||||
|
visible: false
|
||||||
|
onBackgroundClicked: close()
|
||||||
|
onOpened: {
|
||||||
|
modalFocusScope.forceActiveFocus()
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
|
keyboardController.handleKey(event)
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
currentPath = getLastPath()
|
||||||
|
selectedIndex = -1
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
backButtonFocused = false
|
||||||
|
if (browserType === "wallpaper" && !weAvailable) {
|
||||||
|
discoverWallpaperEngine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCurrentPathChanged: {
|
||||||
|
selectedFilePath = ""
|
||||||
|
selectedFileName = ""
|
||||||
|
selectedFileIsDir = false
|
||||||
|
}
|
||||||
|
onSelectedIndexChanged: {
|
||||||
|
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
||||||
|
selectedFilePath = ""
|
||||||
|
selectedFileName = ""
|
||||||
|
selectedFileIsDir = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderListModel {
|
||||||
|
id: folderModel
|
||||||
|
|
||||||
|
showDirsFirst: true
|
||||||
|
showDotAndDotDot: false
|
||||||
|
showHidden: fileBrowserModal.showHiddenFiles
|
||||||
|
nameFilters: fileExtensions
|
||||||
|
showFiles: true
|
||||||
|
showDirs: true
|
||||||
|
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: keyboardController
|
||||||
|
|
||||||
|
property int totalItems: folderModel.count
|
||||||
|
property int gridColumns: 5
|
||||||
|
|
||||||
|
function handleKey(event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
close()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// F10 toggles keyboard hints
|
||||||
|
if (event.key === Qt.Key_F10) {
|
||||||
|
showKeyboardHints = !showKeyboardHints
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// F1 or I key for file information
|
||||||
|
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
||||||
|
showFileInfo = !showFileInfo
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Alt+Left or Backspace to go back
|
||||||
|
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
||||||
|
if (currentPath !== homeDir) {
|
||||||
|
navigateUp()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!keyboardNavigationActive) {
|
||||||
|
if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right) {
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Backtab:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = totalItems - 1
|
||||||
|
} else if (selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
selectedIndex = totalItems - 1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Left:
|
||||||
|
if (backButtonFocused)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Right:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Up:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
// Go to first row, appropriate column
|
||||||
|
var col = selectedIndex % gridColumns
|
||||||
|
selectedIndex = Math.min(col, totalItems - 1)
|
||||||
|
} else if (selectedIndex >= gridColumns) {
|
||||||
|
// Move up one row
|
||||||
|
selectedIndex -= gridColumns
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
// At top row, go to back button
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Down:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else {
|
||||||
|
// Move down one row if possible
|
||||||
|
var newIndex = selectedIndex + gridColumns
|
||||||
|
if (newIndex < totalItems) {
|
||||||
|
selectedIndex = newIndex
|
||||||
|
} else {
|
||||||
|
// If can't go down a full row, go to last item in the column if exists
|
||||||
|
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
|
||||||
|
var col = selectedIndex % gridColumns
|
||||||
|
var targetIndex = lastRowStart + col
|
||||||
|
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
||||||
|
selectedIndex = targetIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
case Qt.Key_Space:
|
||||||
|
if (backButtonFocused)
|
||||||
|
navigateUp()
|
||||||
|
else if (selectedIndex >= 0 && selectedIndex < totalItems)
|
||||||
|
// Trigger selection by setting the grid's current index and using signal
|
||||||
|
fileBrowserModal.keyboardFileSelection(selectedIndex)
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: keyboardSelectionTimer
|
||||||
|
|
||||||
|
property int targetIndex: -1
|
||||||
|
|
||||||
|
interval: 1
|
||||||
|
onTriggered: {
|
||||||
|
// Access the currently selected item through model role names
|
||||||
|
// This will work because QML models expose role data
|
||||||
|
executeKeyboardSelection(targetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: weDiscoveryProcess
|
||||||
|
|
||||||
|
property string wePath: ""
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
fileBrowserModal.weAvailable = true
|
||||||
|
fileBrowserModal.wePath = wePath
|
||||||
|
} else {
|
||||||
|
currentPathIndex++
|
||||||
|
checkNextPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: browserIcon
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: browserTitle
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "movie"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: weMode ? Theme.primary : Theme.surfaceText
|
||||||
|
visible: weAvailable && browserType === "wallpaper"
|
||||||
|
onClicked: {
|
||||||
|
weMode = !weMode
|
||||||
|
if (weMode) {
|
||||||
|
navigateTo(wePath)
|
||||||
|
} else {
|
||||||
|
navigateTo(getLastPath())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "info"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
|
||||||
|
opacity: currentPath !== homeDir ? 1 : 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "arrow_back"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: backButtonMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: currentPath !== homeDir
|
||||||
|
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: currentPath !== homeDir
|
||||||
|
onClicked: navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: fileBrowserModal.currentPath.replace("file://", "")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width - 40 - Theme.spacingS
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
maximumLineCount: 1
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: fileGrid
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 80
|
||||||
|
clip: true
|
||||||
|
cellWidth: weMode ? 255 : 150
|
||||||
|
cellHeight: weMode ? 215 : 130
|
||||||
|
cacheBuffer: 260
|
||||||
|
model: folderModel
|
||||||
|
currentIndex: selectedIndex
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive && currentIndex >= 0)
|
||||||
|
positionViewAtIndex(currentIndex, GridView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: StyledRect {
|
||||||
|
id: delegateRoot
|
||||||
|
|
||||||
|
required property bool fileIsDir
|
||||||
|
required property string filePath
|
||||||
|
required property string fileName
|
||||||
|
required property url fileURL
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: weMode ? 245 : 140
|
||||||
|
height: weMode ? 205 : 120
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||||
|
return Theme.surfacePressed
|
||||||
|
|
||||||
|
return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
}
|
||||||
|
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline
|
||||||
|
border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0
|
||||||
|
// Update file info when this item gets selected via keyboard or initially
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for selectedIndex changes to update file info during keyboard navigation
|
||||||
|
Connections {
|
||||||
|
function onSelectedIndexChanged() {
|
||||||
|
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
target: fileBrowserModal
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: weMode ? 225 : 80
|
||||||
|
height: weMode ? 165 : 60
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
CachingImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
|
||||||
|
property int weExtIndex: 0
|
||||||
|
source: {
|
||||||
|
if (weMode && delegateRoot.fileIsDir) {
|
||||||
|
return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
|
||||||
|
}
|
||||||
|
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
|
||||||
|
}
|
||||||
|
onStatusChanged: {
|
||||||
|
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
|
||||||
|
if (weExtIndex < weExtensions.length - 1) {
|
||||||
|
weExtIndex++
|
||||||
|
source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
|
||||||
|
} else {
|
||||||
|
source = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
visible: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir)
|
||||||
|
maxCacheSize: weMode ? 225 : 80
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "description"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "folder"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
visible: delegateRoot.fileIsDir && !weMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: delegateRoot.fileName || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: 120
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
// Update selected file info and index first
|
||||||
|
selectedIndex = delegateRoot.index
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
if (weMode && delegateRoot.fileIsDir) {
|
||||||
|
var sceneId = delegateRoot.filePath.split("/").pop()
|
||||||
|
fileSelected("we:" + sceneId)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
} else if (delegateRoot.fileIsDir) {
|
||||||
|
navigateTo(delegateRoot.filePath)
|
||||||
|
} else {
|
||||||
|
fileSelected(delegateRoot.filePath)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle keyboard selection
|
||||||
|
Connections {
|
||||||
|
function onKeyboardSelectionRequestedChanged() {
|
||||||
|
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) {
|
||||||
|
fileBrowserModal.keyboardSelectionRequested = false
|
||||||
|
selectedIndex = delegateRoot.index
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
if (weMode && delegateRoot.fileIsDir) {
|
||||||
|
var sceneId = delegateRoot.filePath.split("/").pop()
|
||||||
|
fileSelected("we:" + sceneId)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
} else if (delegateRoot.fileIsDir) {
|
||||||
|
navigateTo(delegateRoot.filePath)
|
||||||
|
} else {
|
||||||
|
fileSelected(delegateRoot.filePath)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: fileBrowserModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveRow
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
height: saveMode ? 40 : 0
|
||||||
|
visible: saveMode
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: fileNameInput
|
||||||
|
|
||||||
|
width: parent.width - saveButton.width - Theme.spacingM
|
||||||
|
height: 40
|
||||||
|
text: defaultFileName
|
||||||
|
placeholderText: "Enter filename..."
|
||||||
|
ignoreLeftRightKeys: false
|
||||||
|
focus: saveMode
|
||||||
|
topPadding: Theme.spacingS
|
||||||
|
bottomPadding: Theme.spacingS
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (saveMode)
|
||||||
|
Qt.callLater(() => {
|
||||||
|
forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onAccepted: {
|
||||||
|
if (text.trim() !== "") {
|
||||||
|
// Remove file:// protocol from currentPath if present for proper construction
|
||||||
|
var basePath = currentPath.replace(/^file:\/\//, '')
|
||||||
|
var fullPath = basePath + "/" + text.trim()
|
||||||
|
// Ensure consistent path format - remove any double slashes and normalize
|
||||||
|
fullPath = fullPath.replace(/\/+/g, '/')
|
||||||
|
handleSaveFile(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
id: saveButton
|
||||||
|
|
||||||
|
width: 80
|
||||||
|
height: 40
|
||||||
|
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Save"
|
||||||
|
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
stateColor: Theme.primary
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
enabled: fileNameInput.text.trim() !== ""
|
||||||
|
onClicked: {
|
||||||
|
if (fileNameInput.text.trim() !== "") {
|
||||||
|
// Remove file:// protocol from currentPath if present for proper construction
|
||||||
|
var basePath = currentPath.replace(/^file:\/\//, '')
|
||||||
|
var fullPath = basePath + "/" + fileNameInput.text.trim()
|
||||||
|
// Ensure consistent path format - remove any double slashes and normalize
|
||||||
|
fullPath = fullPath.replace(/\/+/g, '/')
|
||||||
|
handleSaveFile(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardHints {
|
||||||
|
id: keyboardHints
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
showHints: fileBrowserModal.showKeyboardHints
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo {
|
||||||
|
id: fileInfo
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
width: 300
|
||||||
|
showFileInfo: fileBrowserModal.showFileInfo
|
||||||
|
selectedIndex: fileBrowserModal.selectedIndex
|
||||||
|
sourceFolderModel: folderModel
|
||||||
|
currentPath: fileBrowserModal.currentPath
|
||||||
|
currentFileName: fileBrowserModal.selectedFileName
|
||||||
|
currentFileIsDir: fileBrowserModal.selectedFileIsDir
|
||||||
|
currentFileExtension: {
|
||||||
|
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
|
||||||
|
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite confirmation dialog
|
||||||
|
Item {
|
||||||
|
id: overwriteDialog
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: showOverwriteConfirmation
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
fileSelected(pendingFilePath)
|
||||||
|
pendingFilePath = ""
|
||||||
|
Qt.callLater(() => fileBrowserModal.close())
|
||||||
|
}
|
||||||
|
|
||||||
|
focus: showOverwriteConfirmation
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.shadowStrong
|
||||||
|
opacity: 0.8
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 400
|
||||||
|
height: 160
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("File Already Exists")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("A file with this name already exists. Do you want to overwrite it?")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 80
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 90
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("Overwrite")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.background
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: overwriteArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
fileSelected(pendingFilePath)
|
||||||
|
pendingFilePath = ""
|
||||||
|
Qt.callLater(() => fileBrowserModal.close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
237
Modals/FileBrowser/FileInfo.qml
Normal file
237
Modals/FileBrowser/FileInfo.qml
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtCore
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool showFileInfo: false
|
||||||
|
property int selectedIndex: -1
|
||||||
|
property var sourceFolderModel: null
|
||||||
|
property string currentPath: ""
|
||||||
|
|
||||||
|
height: 200
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
|
border.color: Theme.secondary
|
||||||
|
border.width: 2
|
||||||
|
opacity: showFileInfo ? 1 : 0
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
onShowFileInfoChanged: {
|
||||||
|
if (showFileInfo && currentFileName && currentPath) {
|
||||||
|
const fullPath = currentPath + "/" + currentFileName
|
||||||
|
fileStatProcess.selectedFilePath = fullPath
|
||||||
|
fileStatProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: fileStatProcess
|
||||||
|
command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath]
|
||||||
|
property string selectedFilePath: ""
|
||||||
|
property var fileStats: null
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text && text.trim()) {
|
||||||
|
const parts = text.trim().split('|')
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
fileStatProcess.fileStats = {
|
||||||
|
"modifiedTime": parts[0],
|
||||||
|
"permissions": parts[1],
|
||||||
|
"size": parseInt(parts[2]) || 0,
|
||||||
|
"fullPath": parts[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: function (exitCode) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
property string currentFileName: ""
|
||||||
|
property bool currentFileIsDir: false
|
||||||
|
property string currentFileExtension: ""
|
||||||
|
|
||||||
|
onCurrentFileNameChanged: {
|
||||||
|
if (showFileInfo && currentFileName && currentPath) {
|
||||||
|
const fullPath = currentPath + "/" + currentFileName
|
||||||
|
if (fullPath !== fileStatProcess.selectedFilePath) {
|
||||||
|
fileStatProcess.selectedFilePath = fullPath
|
||||||
|
fileStatProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileInfo(filePath, fileName, isDirectory) {
|
||||||
|
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
|
||||||
|
fileStatProcess.selectedFilePath = filePath
|
||||||
|
currentFileName = fileName || ""
|
||||||
|
currentFileIsDir = isDirectory || false
|
||||||
|
|
||||||
|
let ext = ""
|
||||||
|
if (!isDirectory && fileName) {
|
||||||
|
const lastDot = fileName.lastIndexOf('.')
|
||||||
|
if (lastDot > 0) {
|
||||||
|
ext = fileName.substring(lastDot + 1).toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentFileExtension = ext
|
||||||
|
|
||||||
|
if (showFileInfo) {
|
||||||
|
fileStatProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var currentFileDisplayData: {
|
||||||
|
if (selectedIndex < 0 || !sourceFolderModel) {
|
||||||
|
return {
|
||||||
|
"exists": false,
|
||||||
|
"name": "No selection",
|
||||||
|
"type": "",
|
||||||
|
"size": "",
|
||||||
|
"modified": "",
|
||||||
|
"permissions": "",
|
||||||
|
"extension": "",
|
||||||
|
"position": "N/A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasValidFile = currentFileName !== ""
|
||||||
|
return {
|
||||||
|
"exists": hasValidFile,
|
||||||
|
"name": hasValidFile ? currentFileName : "Loading...",
|
||||||
|
"type": currentFileIsDir ? "Directory" : "File",
|
||||||
|
"size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...",
|
||||||
|
"modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...",
|
||||||
|
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
|
||||||
|
"extension": currentFileExtension,
|
||||||
|
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "info"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.secondary
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "File Information"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.name
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.size
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
visible: currentFileDisplayData.exists && !currentFileIsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.modified
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: currentFileDisplayData.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.permissions
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
visible: currentFileDisplayData.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.position
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "F1/I: Toggle • F10: Help"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes === 0 || !bytes) {
|
||||||
|
return "0 B"
|
||||||
|
}
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(dateTimeString) {
|
||||||
|
if (!dateTimeString) {
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
const parts = dateTimeString.split(' ')
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return parts[0] + " " + parts[1].split('.')[0]
|
||||||
|
}
|
||||||
|
return dateTimeString
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Modals/FileBrowser/KeyboardHints.qml
Normal file
50
Modals/FileBrowser/KeyboardHints.qml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool showHints: false
|
||||||
|
|
||||||
|
height: 80
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 2
|
||||||
|
opacity: showHints ? 1 : 0
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtCore
|
|
||||||
import Qt.labs.folderlistmodel
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: fileBrowserModal
|
|
||||||
objectName: "fileBrowserModal"
|
|
||||||
allowStacking: true
|
|
||||||
|
|
||||||
signal fileSelected(string path)
|
|
||||||
|
|
||||||
property string homeDir: StandardPaths.writableLocation(
|
|
||||||
StandardPaths.HomeLocation)
|
|
||||||
property string currentPath: ""
|
|
||||||
property var fileExtensions: ["*.*"]
|
|
||||||
property alias filterExtensions: fileBrowserModal.fileExtensions
|
|
||||||
property string browserTitle: "Select File"
|
|
||||||
property string browserIcon: "folder_open"
|
|
||||||
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
|
|
||||||
property bool showHiddenFiles: false
|
|
||||||
|
|
||||||
FolderListModel {
|
|
||||||
id: folderModel
|
|
||||||
showDirsFirst: true
|
|
||||||
showDotAndDotDot: false
|
|
||||||
showHidden: fileBrowserModal.showHiddenFiles
|
|
||||||
nameFilters: fileExtensions
|
|
||||||
showFiles: true
|
|
||||||
showDirs: true
|
|
||||||
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function isImageFile(fileName) {
|
|
||||||
if (!fileName)
|
|
||||||
return false
|
|
||||||
var ext = fileName.toLowerCase().split('.').pop()
|
|
||||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastPath() {
|
|
||||||
var lastPath = ""
|
|
||||||
if (browserType === "wallpaper") {
|
|
||||||
lastPath = SessionData.wallpaperLastPath
|
|
||||||
} else if (browserType === "profile") {
|
|
||||||
lastPath = SessionData.profileLastPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastPath && lastPath !== "") {
|
|
||||||
return lastPath
|
|
||||||
}
|
|
||||||
return homeDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveLastPath(path) {
|
|
||||||
if (browserType === "wallpaper") {
|
|
||||||
SessionData.setWallpaperLastPath(path)
|
|
||||||
} else if (browserType === "profile") {
|
|
||||||
SessionData.setProfileLastPath(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
currentPath = getLastPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 800
|
|
||||||
height: 600
|
|
||||||
enableShadow: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
onBackgroundClicked: close()
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
var startPath = getLastPath()
|
|
||||||
currentPath = startPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentPathChanged: {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateUp() {
|
|
||||||
var path = currentPath
|
|
||||||
|
|
||||||
if (path === homeDir) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastSlash = path.lastIndexOf('/')
|
|
||||||
if (lastSlash > 0) {
|
|
||||||
var newPath = path.substring(0, lastSlash)
|
|
||||||
if (newPath.length < homeDir.length) {
|
|
||||||
currentPath = homeDir
|
|
||||||
saveLastPath(homeDir)
|
|
||||||
} else {
|
|
||||||
currentPath = newPath
|
|
||||||
saveLastPath(newPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateTo(path) {
|
|
||||||
currentPath = path
|
|
||||||
saveLastPath(path) // Save the path when navigating
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: browserIcon
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: browserTitle
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: fileBrowserModal.close()
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: mouseArea.containsMouse
|
|
||||||
&& currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
|
|
||||||
opacity: currentPath !== homeDir ? 1.0 : 0.0
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "arrow_back"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: currentPath !== homeDir
|
|
||||||
cursorShape: currentPath
|
|
||||||
!== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: currentPath !== homeDir
|
|
||||||
onClicked: navigateUp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: fileBrowserModal.currentPath.replace("file://", "")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
width: parent.width - 40 - Theme.spacingS
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
maximumLineCount: 1
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: fileGrid
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 80
|
|
||||||
clip: true
|
|
||||||
cellWidth: 150
|
|
||||||
cellHeight: 130
|
|
||||||
cacheBuffer: 260
|
|
||||||
|
|
||||||
model: folderModel
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: StyledRect {
|
|
||||||
id: delegateRoot
|
|
||||||
|
|
||||||
required property bool fileIsDir
|
|
||||||
required property string filePath
|
|
||||||
required property string fileName
|
|
||||||
required property url fileURL
|
|
||||||
|
|
||||||
width: 140
|
|
||||||
height: 120
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: mouseArea.containsMouse ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 80
|
|
||||||
height: 60
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
CachingImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
visible: !delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)
|
|
||||||
maxCacheSize: 80
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "description"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
visible: !delegateRoot.fileIsDir
|
|
||||||
&& !isImageFile(delegateRoot.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "folder"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
visible: delegateRoot.fileIsDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: delegateRoot.fileName || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: 120
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
maximumLineCount: 2
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (delegateRoot.fileIsDir) {
|
|
||||||
navigateTo(delegateRoot.filePath)
|
|
||||||
} else {
|
|
||||||
fileSelected(delegateRoot.filePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -13,7 +11,6 @@ DankModal {
|
|||||||
property bool networkInfoModalVisible: false
|
property bool networkInfoModalVisible: false
|
||||||
property string networkSSID: ""
|
property string networkSSID: ""
|
||||||
property var networkData: null
|
property var networkData: null
|
||||||
property string networkDetails: ""
|
|
||||||
|
|
||||||
function showNetworkInfo(ssid, data) {
|
function showNetworkInfo(ssid, data) {
|
||||||
networkSSID = ssid
|
networkSSID = ssid
|
||||||
@@ -28,21 +25,17 @@ DankModal {
|
|||||||
close()
|
close()
|
||||||
networkSSID = ""
|
networkSSID = ""
|
||||||
networkData = null
|
networkData = null
|
||||||
networkDetails = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: networkInfoModalVisible
|
visible: networkInfoModalVisible
|
||||||
width: 600
|
width: 600
|
||||||
height: 500
|
height: 500
|
||||||
enableShadow: true
|
enableShadow: true
|
||||||
onBackgroundClicked: {
|
onBackgroundClicked: hideDialog()
|
||||||
hideDialog()
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
networkSSID = ""
|
networkSSID = ""
|
||||||
networkData = null
|
networkData = null
|
||||||
networkDetails = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,92 +63,51 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: "Details for \"" + networkSSID + "\""
|
text: `Details for "${networkSSID}"`
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
width: parent.width
|
width: parent.width
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
iconName: "close"
|
iconName: "close"
|
||||||
iconSize: Theme.iconSize - 4
|
iconSize: Theme.iconSize - 4
|
||||||
iconColor: Theme.surfaceText
|
iconColor: Theme.surfaceText
|
||||||
hoverColor: Theme.errorHover
|
onClicked: root.hideDialog()
|
||||||
onClicked: {
|
|
||||||
root.hideDialog()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Flickable {
|
Rectangle {
|
||||||
|
id: detailsRect
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - 140
|
height: parent.height - 140
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceHover
|
||||||
|
border.color: Theme.outlineStrong
|
||||||
|
border.width: 1
|
||||||
clip: true
|
clip: true
|
||||||
contentWidth: width
|
|
||||||
contentHeight: detailsRect.height
|
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
DankFlickable {
|
||||||
interactive: true
|
anchors.fill: parent
|
||||||
flickDeceleration: 1500
|
anchors.margins: Theme.spacingM
|
||||||
maximumFlickVelocity: 2000
|
contentHeight: detailsText.contentHeight
|
||||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
|
||||||
pressDelay: 0
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: event => {
|
|
||||||
let delta = event.pixelDelta.y
|
|
||||||
!== 0 ? event.pixelDelta.y
|
|
||||||
* 1.8 : event.angleDelta.y / 120 * 60
|
|
||||||
let newY = parent.contentY - delta
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: detailsRect
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: Math.max(
|
|
||||||
parent.parent.height,
|
|
||||||
detailsText.contentHeight + Theme.spacingM * 2)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: detailsText
|
id: detailsText
|
||||||
|
|
||||||
anchors.fill: parent
|
width: parent.width
|
||||||
anchors.margins: Theme.spacingM
|
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
|
||||||
text: NetworkService.networkInfoDetails.replace(
|
|
||||||
/\\n/g,
|
|
||||||
'\n') || "No information available"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
lineHeight: 1.5
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -165,14 +117,10 @@ DankModal {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: Math.max(
|
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
|
||||||
70,
|
|
||||||
closeText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
height: 36
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: closeArea.containsMouse ? Qt.darker(
|
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
Theme.primary,
|
|
||||||
1.1) : Theme.primary
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: closeText
|
id: closeText
|
||||||
@@ -190,9 +138,7 @@ DankModal {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: root.hideDialog()
|
||||||
root.hideDialog()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
@@ -200,10 +146,17 @@ DankModal {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
import qs.Modules.Notifications.Center
|
import qs.Modules.Notifications.Center
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -12,28 +9,6 @@ import qs.Widgets
|
|||||||
DankModal {
|
DankModal {
|
||||||
id: notificationModal
|
id: notificationModal
|
||||||
|
|
||||||
width: 500
|
|
||||||
height: 700
|
|
||||||
visible: false
|
|
||||||
onBackgroundClicked: hide()
|
|
||||||
onDialogClosed: {
|
|
||||||
notificationModalOpen = false
|
|
||||||
modalKeyboardController.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
|
||||||
modalKeyboardController.handleKey(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationKeyboardController {
|
|
||||||
id: modalKeyboardController
|
|
||||||
listView: null
|
|
||||||
isOpen: notificationModal.notificationModalOpen
|
|
||||||
onClose: function () {
|
|
||||||
notificationModal.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool notificationModalOpen: false
|
property bool notificationModalOpen: false
|
||||||
property var notificationListRef: null
|
property var notificationListRef: null
|
||||||
|
|
||||||
@@ -42,10 +17,21 @@ DankModal {
|
|||||||
NotificationService.onOverlayOpen()
|
NotificationService.onOverlayOpen()
|
||||||
open()
|
open()
|
||||||
modalKeyboardController.reset()
|
modalKeyboardController.reset()
|
||||||
|
|
||||||
if (modalKeyboardController && notificationListRef) {
|
if (modalKeyboardController && notificationListRef) {
|
||||||
modalKeyboardController.listView = notificationListRef
|
modalKeyboardController.listView = notificationListRef
|
||||||
modalKeyboardController.rebuildFlatNavigation()
|
modalKeyboardController.rebuildFlatNavigation()
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
modalKeyboardController.keyboardNavigationActive = true
|
||||||
|
modalKeyboardController.selectedFlatIndex = 0
|
||||||
|
modalKeyboardController.updateSelectedIdFromIndex()
|
||||||
|
if (notificationListRef) {
|
||||||
|
notificationListRef.keyboardActive = true
|
||||||
|
notificationListRef.currentIndex = 0
|
||||||
|
}
|
||||||
|
modalKeyboardController.selectionVersion++
|
||||||
|
modalKeyboardController.ensureVisible()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,32 +43,54 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (shouldBeVisible)
|
if (shouldBeVisible) {
|
||||||
hide()
|
hide()
|
||||||
else
|
} else {
|
||||||
show()
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 500
|
||||||
|
height: 700
|
||||||
|
visible: false
|
||||||
|
onBackgroundClicked: hide()
|
||||||
|
onShouldBeVisibleChanged: (shouldBeVisible) => {
|
||||||
|
if (!shouldBeVisible) {
|
||||||
|
notificationModalOpen = false
|
||||||
|
modalKeyboardController.reset()
|
||||||
|
NotificationService.onOverlayClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: (event) => modalKeyboardController.handleKey(event)
|
||||||
|
|
||||||
|
NotificationKeyboardController {
|
||||||
|
id: modalKeyboardController
|
||||||
|
|
||||||
|
listView: null
|
||||||
|
isOpen: notificationModal.notificationModalOpen
|
||||||
|
onClose: () => notificationModal.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open() {
|
function open(): string {
|
||||||
notificationModal.show()
|
notificationModal.show();
|
||||||
return "NOTIFICATION_MODAL_OPEN_SUCCESS"
|
return "NOTIFICATION_MODAL_OPEN_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close(): string {
|
||||||
notificationModal.hide()
|
notificationModal.hide();
|
||||||
return "NOTIFICATION_MODAL_CLOSE_SUCCESS"
|
return "NOTIFICATION_MODAL_CLOSE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle(): string {
|
||||||
notificationModal.toggle()
|
notificationModal.toggle();
|
||||||
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS"
|
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
target: "notifications"
|
target: "notifications"
|
||||||
}
|
}
|
||||||
|
|
||||||
property Component notificationContent: Component {
|
content: Component {
|
||||||
Item {
|
Item {
|
||||||
id: notificationKeyHandler
|
id: notificationKeyHandler
|
||||||
|
|
||||||
@@ -95,11 +103,13 @@ DankModal {
|
|||||||
|
|
||||||
NotificationHeader {
|
NotificationHeader {
|
||||||
id: notificationHeader
|
id: notificationHeader
|
||||||
|
|
||||||
keyboardController: modalKeyboardController
|
keyboardController: modalKeyboardController
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationSettings {
|
NotificationSettings {
|
||||||
id: notificationSettings
|
id: notificationSettings
|
||||||
|
|
||||||
expanded: notificationHeader.showSettings
|
expanded: notificationHeader.showSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +119,6 @@ DankModal {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - y
|
height: parent.height - y
|
||||||
keyboardController: modalKeyboardController
|
keyboardController: modalKeyboardController
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
notificationModal.notificationListRef = notificationList
|
notificationModal.notificationListRef = notificationList
|
||||||
if (modalKeyboardController) {
|
if (modalKeyboardController) {
|
||||||
@@ -118,18 +127,21 @@ DankModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationKeyboardHints {
|
NotificationKeyboardHints {
|
||||||
id: keyboardHints
|
id: keyboardHints
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.margins: Theme.spacingL
|
anchors.margins: Theme.spacingL
|
||||||
showHints: modalKeyboardController.showKeyboardHints
|
showHints: modalKeyboardController.showKeyboardHints
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content: notificationContent
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string powerConfirmAction: ""
|
|
||||||
property string powerConfirmTitle: ""
|
|
||||||
property string powerConfirmMessage: ""
|
|
||||||
|
|
||||||
function show(action, title, message) {
|
|
||||||
powerConfirmAction = action
|
|
||||||
powerConfirmTitle = title
|
|
||||||
powerConfirmMessage = message
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function executePowerAction(action) {
|
|
||||||
switch (action) {
|
|
||||||
case "logout":
|
|
||||||
CompositorService.logout()
|
|
||||||
break
|
|
||||||
case "suspend":
|
|
||||||
SessionService.suspend()
|
|
||||||
break
|
|
||||||
case "reboot":
|
|
||||||
SessionService.reboot()
|
|
||||||
break
|
|
||||||
case "poweroff":
|
|
||||||
SessionService.poweroff()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
width: 350
|
|
||||||
height: 160
|
|
||||||
enableShadow: false
|
|
||||||
onBackgroundClicked: {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: powerConfirmTitle
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: {
|
|
||||||
switch (powerConfirmAction) {
|
|
||||||
case "poweroff":
|
|
||||||
return Theme.error
|
|
||||||
case "reboot":
|
|
||||||
return Theme.warning
|
|
||||||
default:
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.weight: Font.Medium
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: powerConfirmMessage
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
height: Theme.spacingS
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 120
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: cancelButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Cancel"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cancelButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 120
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
let baseColor
|
|
||||||
switch (powerConfirmAction) {
|
|
||||||
case "poweroff":
|
|
||||||
baseColor = Theme.error
|
|
||||||
break
|
|
||||||
case "reboot":
|
|
||||||
baseColor = Theme.warning
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
baseColor = Theme.primary
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return confirmButton.containsMouse ? Qt.rgba(
|
|
||||||
baseColor.r,
|
|
||||||
baseColor.g,
|
|
||||||
baseColor.b,
|
|
||||||
0.9) : baseColor
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Confirm"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primaryText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: confirmButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
close()
|
|
||||||
executePowerAction(powerConfirmAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
406
Modals/PowerMenuModal.qml
Normal file
406
Modals/PowerMenuModal.qml
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property int optionCount: SessionService.hibernateSupported ? 5 : 4
|
||||||
|
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
|
||||||
|
function selectOption(action) {
|
||||||
|
close();
|
||||||
|
const actions = {
|
||||||
|
"logout": {
|
||||||
|
"title": "Log Out",
|
||||||
|
"message": "Are you sure you want to log out?"
|
||||||
|
},
|
||||||
|
"suspend": {
|
||||||
|
"title": "Suspend",
|
||||||
|
"message": "Are you sure you want to suspend the system?"
|
||||||
|
},
|
||||||
|
"hibernate": {
|
||||||
|
"title": "Hibernate",
|
||||||
|
"message": "Are you sure you want to hibernate the system?"
|
||||||
|
},
|
||||||
|
"reboot": {
|
||||||
|
"title": "Reboot",
|
||||||
|
"message": "Are you sure you want to reboot the system?"
|
||||||
|
},
|
||||||
|
"poweroff": {
|
||||||
|
"title": "Power Off",
|
||||||
|
"message": "Are you sure you want to power off the system?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const selected = actions[action]
|
||||||
|
if (selected) {
|
||||||
|
root.powerActionRequested(action, selected.title, selected.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
width: 320
|
||||||
|
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
|
||||||
|
enableShadow: true
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return close();
|
||||||
|
}
|
||||||
|
onOpened: () => {
|
||||||
|
selectedIndex = 0;
|
||||||
|
modalFocusScope.forceActiveFocus();
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Up:
|
||||||
|
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Down:
|
||||||
|
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
const actions = ["logout", "suspend"];
|
||||||
|
if (SessionService.hibernateSupported) actions.push("hibernate");
|
||||||
|
actions.push("reboot", "poweroff");
|
||||||
|
if (selectedIndex < actions.length) {
|
||||||
|
selectOption(actions[selectedIndex]);
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Options"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 150
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
return close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 0) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (logoutArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === 0 ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "logout"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Log Out"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: logoutArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 0;
|
||||||
|
selectOption("logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 1) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (suspendArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 1 ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === 1 ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "bedtime"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Suspend"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: suspendArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 1;
|
||||||
|
selectOption("suspend");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 2) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (hibernateArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 2 ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === 2 ? 1 : 0
|
||||||
|
visible: SessionService.hibernateSupported
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "ac_unit"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Hibernate"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hibernateArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 2;
|
||||||
|
selectOption("hibernate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
const rebootIndex = SessionService.hibernateSupported ? 3 : 2;
|
||||||
|
if (selectedIndex === rebootIndex) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (rebootArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "restart_alt"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Reboot"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rebootArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = SessionService.hibernateSupported ? 3 : 2;
|
||||||
|
selectOption("reboot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
const powerOffIndex = SessionService.hibernateSupported ? 4 : 3;
|
||||||
|
if (selectedIndex === powerOffIndex) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (powerOffArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "power_settings_new"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Off"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: powerOffArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = SessionService.hibernateSupported ? 4 : 3;
|
||||||
|
selectOption("poweroff");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
import qs.Modules.ProcessList
|
import qs.Modules.ProcessList
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -19,28 +14,31 @@ DankModal {
|
|||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
if (!DgopService.dgopAvailable) {
|
if (!DgopService.dgopAvailable) {
|
||||||
console.warn("ProcessListModal: dgop is not available")
|
console.warn("ProcessListModal: dgop is not available");
|
||||||
return
|
return ;
|
||||||
}
|
}
|
||||||
open()
|
open();
|
||||||
UserInfoService.getUptime()
|
UserInfoService.getUptime();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
close()
|
close();
|
||||||
if (processContextMenu.visible)
|
if (processContextMenu.visible) {
|
||||||
processContextMenu.close()
|
processContextMenu.close();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (!DgopService.dgopAvailable) {
|
if (!DgopService.dgopAvailable) {
|
||||||
console.warn("ProcessListModal: dgop is not available")
|
console.warn("ProcessListModal: dgop is not available");
|
||||||
return
|
return ;
|
||||||
|
}
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
show();
|
||||||
}
|
}
|
||||||
if (shouldBeVisible)
|
|
||||||
hide()
|
|
||||||
else
|
|
||||||
show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
width: 900
|
width: 900
|
||||||
@@ -49,7 +47,9 @@ DankModal {
|
|||||||
backgroundColor: Theme.popupBackground()
|
backgroundColor: Theme.popupBackground()
|
||||||
cornerRadius: Theme.cornerRadius
|
cornerRadius: Theme.cornerRadius
|
||||||
enableShadow: true
|
enableShadow: true
|
||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: () => {
|
||||||
|
return hide();
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: processesTabComponent
|
id: processesTabComponent
|
||||||
@@ -57,18 +57,23 @@ DankModal {
|
|||||||
ProcessesTab {
|
ProcessesTab {
|
||||||
contextMenu: processContextMenu
|
contextMenu: processContextMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: performanceTabComponent
|
id: performanceTabComponent
|
||||||
|
|
||||||
PerformanceTab {}
|
PerformanceTab {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: systemTabComponent
|
id: systemTabComponent
|
||||||
|
|
||||||
SystemTab {}
|
SystemTab {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessContextMenu {
|
ProcessContextMenu {
|
||||||
@@ -79,19 +84,19 @@ DankModal {
|
|||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
Keys.onPressed: function (event) {
|
Keys.onPressed: (event) => {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
processListModal.hide()
|
processListModal.hide();
|
||||||
event.accepted = true
|
event.accepted = true;
|
||||||
} else if (event.key === Qt.Key_1) {
|
} else if (event.key === Qt.Key_1) {
|
||||||
currentTab = 0
|
currentTab = 0;
|
||||||
event.accepted = true
|
event.accepted = true;
|
||||||
} else if (event.key === Qt.Key_2) {
|
} else if (event.key === Qt.Key_2) {
|
||||||
currentTab = 1
|
currentTab = 1;
|
||||||
event.accepted = true
|
event.accepted = true;
|
||||||
} else if (event.key === Qt.Key_3) {
|
} else if (event.key === Qt.Key_3) {
|
||||||
currentTab = 2
|
currentTab = 2;
|
||||||
event.accepted = true
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +138,9 @@ DankModal {
|
|||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@@ -163,16 +170,18 @@ DankModal {
|
|||||||
iconName: "close"
|
iconName: "close"
|
||||||
iconSize: Theme.iconSize - 4
|
iconSize: Theme.iconSize - 4
|
||||||
iconColor: Theme.surfaceText
|
iconColor: Theme.surfaceText
|
||||||
hoverColor: Theme.errorHover
|
onClicked: () => {
|
||||||
onClicked: processListModal.hide()
|
return processListModal.hide();
|
||||||
|
}
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: 52
|
height: 52
|
||||||
color: Theme.surfaceSelected
|
color: Theme.surfaceContainerHigh
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Theme.outlineLight
|
border.color: Theme.outlineLight
|
||||||
border.width: 1
|
border.width: 1
|
||||||
@@ -199,20 +208,11 @@ DankModal {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: {
|
name: {
|
||||||
switch (index) {
|
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||||
case 0:
|
return tabIcons[index] || "tab";
|
||||||
return "list_alt"
|
|
||||||
case 1:
|
|
||||||
return "analytics"
|
|
||||||
case 2:
|
|
||||||
return "settings"
|
|
||||||
default:
|
|
||||||
return "tab"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: currentTab
|
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||||
=== index ? Theme.primary : Theme.surfaceText
|
|
||||||
opacity: currentTab === index ? 1 : 0.7
|
opacity: currentTab === index ? 1 : 0.7
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
@@ -220,15 +220,16 @@ DankModal {
|
|||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData
|
text: modelData
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: currentTab
|
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||||
=== index ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.verticalCenterOffset: -1
|
anchors.verticalCenterOffset: -1
|
||||||
|
|
||||||
@@ -236,8 +237,11 @@ DankModal {
|
|||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -246,8 +250,8 @@ DankModal {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: () => {
|
||||||
currentTab = index
|
currentTab = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,23 +259,29 @@ DankModal {
|
|||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on border.color {
|
Behavior on border.color {
|
||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceLight
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Theme.outlineLight
|
border.color: Theme.outlineLight
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
@@ -290,7 +300,9 @@ DankModal {
|
|||||||
duration: Theme.mediumDuration
|
duration: Theme.mediumDuration
|
||||||
easing.type: Theme.emphasizedEasing
|
easing.type: Theme.emphasizedEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
@@ -308,7 +320,9 @@ DankModal {
|
|||||||
duration: Theme.mediumDuration
|
duration: Theme.mediumDuration
|
||||||
easing.type: Theme.emphasizedEasing
|
easing.type: Theme.emphasizedEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
@@ -326,10 +340,17 @@ DankModal {
|
|||||||
duration: Theme.mediumDuration
|
duration: Theme.mediumDuration
|
||||||
easing.type: Theme.emphasizedEasing
|
easing.type: Theme.emphasizedEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
246
Modals/Settings/PowerSettings.qml
Normal file
246
Modals/Settings/PowerSettings.qml
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: powerTab
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Battery not detected - only AC power settings available"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: !BatteryService.batteryAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: parent.width
|
||||||
|
height: timeoutSection.implicitHeight + Theme.spacingL * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: timeoutSection
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "schedule"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Idle Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - parent.children[0].width - parent.children[1].width - powerCategory.width - Theme.spacingM * 3)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButtonGroup {
|
||||||
|
id: powerCategory
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: BatteryService.batteryAvailable
|
||||||
|
model: ["AC Power", "Battery"]
|
||||||
|
currentIndex: 0
|
||||||
|
selectionMode: "single"
|
||||||
|
checkEnabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: lockDropdown
|
||||||
|
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||||
|
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: "Automatically lock after"
|
||||||
|
options: timeoutOptions
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: powerCategory
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
|
||||||
|
const index = lockDropdown.timeoutValues.indexOf(currentTimeout)
|
||||||
|
lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
|
||||||
|
const index = timeoutValues.indexOf(currentTimeout)
|
||||||
|
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = timeoutOptions.indexOf(value)
|
||||||
|
if (index >= 0) {
|
||||||
|
const timeout = timeoutValues[index]
|
||||||
|
if (powerCategory.currentIndex === 0) {
|
||||||
|
SessionData.setAcLockTimeout(timeout)
|
||||||
|
} else {
|
||||||
|
SessionData.setBatteryLockTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: monitorDropdown
|
||||||
|
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||||
|
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: "Turn off monitors after"
|
||||||
|
options: timeoutOptions
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: powerCategory
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
|
||||||
|
const index = monitorDropdown.timeoutValues.indexOf(currentTimeout)
|
||||||
|
monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
|
||||||
|
const index = timeoutValues.indexOf(currentTimeout)
|
||||||
|
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = timeoutOptions.indexOf(value)
|
||||||
|
if (index >= 0) {
|
||||||
|
const timeout = timeoutValues[index]
|
||||||
|
if (powerCategory.currentIndex === 0) {
|
||||||
|
SessionData.setAcMonitorTimeout(timeout)
|
||||||
|
} else {
|
||||||
|
SessionData.setBatteryMonitorTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: suspendDropdown
|
||||||
|
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||||
|
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: "Suspend system after"
|
||||||
|
options: timeoutOptions
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: powerCategory
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
|
||||||
|
const index = suspendDropdown.timeoutValues.indexOf(currentTimeout)
|
||||||
|
suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
|
||||||
|
const index = timeoutValues.indexOf(currentTimeout)
|
||||||
|
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = timeoutOptions.indexOf(value)
|
||||||
|
if (index >= 0) {
|
||||||
|
const timeout = timeoutValues[index]
|
||||||
|
if (powerCategory.currentIndex === 0) {
|
||||||
|
SessionData.setAcSuspendTimeout(timeout)
|
||||||
|
} else {
|
||||||
|
SessionData.setBatterySuspendTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: hibernateDropdown
|
||||||
|
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||||
|
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: "Hibernate system after"
|
||||||
|
options: timeoutOptions
|
||||||
|
visible: SessionService.hibernateSupported
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: powerCategory
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
|
||||||
|
const index = hibernateDropdown.timeoutValues.indexOf(currentTimeout)
|
||||||
|
hibernateDropdown.currentValue = index >= 0 ? hibernateDropdown.timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
|
||||||
|
const index = timeoutValues.indexOf(currentTimeout)
|
||||||
|
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = timeoutOptions.indexOf(value)
|
||||||
|
if (index >= 0) {
|
||||||
|
const timeout = timeoutValues[index]
|
||||||
|
if (powerCategory.currentIndex === 0) {
|
||||||
|
SessionData.setAcHibernateTimeout(timeout)
|
||||||
|
} else {
|
||||||
|
SessionData.setBatteryHibernateTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
width: parent.width
|
||||||
|
text: "Lock before suspend"
|
||||||
|
description: "Automatically lock the screen when the system prepares to suspend"
|
||||||
|
checked: SessionData.lockBeforeSuspend
|
||||||
|
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Idle monitoring not supported - requires newer Quickshell version"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: !IdleService.idleMonitorAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
Modals/Settings/ProfileSection.qml
Normal file
153
Modals/Settings/ProfileSection.qml
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 110
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: profileImageContainer
|
||||||
|
|
||||||
|
width: 80
|
||||||
|
height: 80
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankCircularImage {
|
||||||
|
id: profileImage
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
imageSource: {
|
||||||
|
if (PortalService.profileImage === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (PortalService.profileImage.startsWith("/")) {
|
||||||
|
return "file://" + PortalService.profileImage;
|
||||||
|
}
|
||||||
|
return PortalService.profileImage;
|
||||||
|
}
|
||||||
|
fallbackIcon: "person"
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.7)
|
||||||
|
visible: profileMouseArea.containsMouse
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: Qt.rgba(255, 255, 255, 0.9)
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "edit"
|
||||||
|
size: 16
|
||||||
|
color: "black"
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
if (root.parentModal) {
|
||||||
|
root.parentModal.allowFocusOverride = true;
|
||||||
|
root.parentModal.shouldHaveFocus = false;
|
||||||
|
if (root.parentModal.profileBrowser) {
|
||||||
|
root.parentModal.profileBrowser.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: Qt.rgba(255, 255, 255, 0.9)
|
||||||
|
visible: profileImage.hasImage
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "close"
|
||||||
|
size: 16
|
||||||
|
color: "black"
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
return PortalService.setProfileImage("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: profileMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
propagateComposedEvents: true
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: 120
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: UserInfoService.fullName || "User"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: DgopService.distribution || "Linux"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
171
Modals/Settings/SettingsContent.qml
Normal file
171
Modals/Settings/SettingsContent.qml
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.Settings
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int currentIndex: 0
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 0
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: 0
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: personalizationLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 0
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: Component {
|
||||||
|
PersonalizationTab {
|
||||||
|
parentModal: root.parentModal
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: timeLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 1
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: TimeTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: weatherLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 2
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: WeatherTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: topBarLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 3
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: TopBarTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: widgetsLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 4
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: WidgetTweaksTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: dockLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 5
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: Component {
|
||||||
|
DockTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: displaysLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 6
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: DisplaysTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: launcherLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 7
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: LauncherTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: themeColorsLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 8
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: ThemeColorsTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: powerLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 9
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: PowerSettings {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: aboutLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 10
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: AboutTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
199
Modals/Settings/SettingsModal.qml
Normal file
199
Modals/Settings/SettingsModal.qml
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
|
import qs.Modules.Settings
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: settingsModal
|
||||||
|
|
||||||
|
property Component settingsContent
|
||||||
|
property alias profileBrowser: profileBrowser
|
||||||
|
|
||||||
|
signal closingModal()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "settingsModal"
|
||||||
|
width: 800
|
||||||
|
height: 750
|
||||||
|
visible: false
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return hide();
|
||||||
|
}
|
||||||
|
content: settingsContent
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
settingsModal.show();
|
||||||
|
return "SETTINGS_OPEN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
settingsModal.hide();
|
||||||
|
return "SETTINGS_CLOSE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
settingsModal.toggle();
|
||||||
|
return "SETTINGS_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function browse(type: string) {
|
||||||
|
if (type === "wallpaper") {
|
||||||
|
wallpaperBrowser.allowStacking = false;
|
||||||
|
wallpaperBrowser.open();
|
||||||
|
} else if (type === "profile") {
|
||||||
|
profileBrowser.allowStacking = false;
|
||||||
|
profileBrowser.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: profileBrowser
|
||||||
|
|
||||||
|
allowStacking: true
|
||||||
|
browserTitle: "Select Profile Image"
|
||||||
|
browserIcon: "person"
|
||||||
|
browserType: "profile"
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
|
onFileSelected: (path) => {
|
||||||
|
PortalService.setProfileImage(path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
onDialogClosed: () => {
|
||||||
|
if (settingsModal) {
|
||||||
|
settingsModal.allowFocusOverride = false;
|
||||||
|
settingsModal.shouldHaveFocus = Qt.binding(() => {
|
||||||
|
return settingsModal.shouldBeVisible;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
allowStacking = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: wallpaperBrowser
|
||||||
|
|
||||||
|
allowStacking: true
|
||||||
|
browserTitle: "Select Wallpaper"
|
||||||
|
browserIcon: "wallpaper"
|
||||||
|
browserType: "wallpaper"
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
|
onFileSelected: (path) => {
|
||||||
|
SessionData.setWallpaper(path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
onDialogClosed: () => {
|
||||||
|
allowStacking = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsContent: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
anchors.bottomMargin: Theme.spacingL
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 35
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "settings"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
return settingsModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 35
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
SettingsSidebar {
|
||||||
|
id: sidebar
|
||||||
|
|
||||||
|
parentModal: settingsModal
|
||||||
|
onCurrentIndexChanged: content.currentIndex = currentIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsContent {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
width: parent.width - sidebar.width
|
||||||
|
height: parent.height
|
||||||
|
parentModal: settingsModal
|
||||||
|
currentIndex: sidebar.currentIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
136
Modals/Settings/SettingsSidebar.qml
Normal file
136
Modals/Settings/SettingsSidebar.qml
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Settings
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sidebarContainer
|
||||||
|
|
||||||
|
property int currentIndex: 0
|
||||||
|
property var parentModal: null
|
||||||
|
readonly property var sidebarItems: [{
|
||||||
|
"text": "Personalization",
|
||||||
|
"icon": "person"
|
||||||
|
}, {
|
||||||
|
"text": "Time & Date",
|
||||||
|
"icon": "schedule"
|
||||||
|
}, {
|
||||||
|
"text": "Weather",
|
||||||
|
"icon": "cloud"
|
||||||
|
}, {
|
||||||
|
"text": "Top Bar",
|
||||||
|
"icon": "toolbar"
|
||||||
|
}, {
|
||||||
|
"text": "Widgets",
|
||||||
|
"icon": "widgets"
|
||||||
|
}, {
|
||||||
|
"text": "Dock",
|
||||||
|
"icon": "dock_to_bottom"
|
||||||
|
}, {
|
||||||
|
"text": "Displays",
|
||||||
|
"icon": "monitor"
|
||||||
|
}, {
|
||||||
|
"text": "Launcher",
|
||||||
|
"icon": "apps"
|
||||||
|
}, {
|
||||||
|
"text": "Theme & Colors",
|
||||||
|
"icon": "palette"
|
||||||
|
}, {
|
||||||
|
"text": "Power",
|
||||||
|
"icon": "power_settings_new"
|
||||||
|
}, {
|
||||||
|
"text": "About",
|
||||||
|
"icon": "info"
|
||||||
|
}]
|
||||||
|
|
||||||
|
width: 270
|
||||||
|
height: parent.height
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
anchors.topMargin: Theme.spacingM + 2
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
ProfileSection {
|
||||||
|
parentModal: sidebarContainer.parentModal
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.spacingL
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: sidebarRepeater
|
||||||
|
|
||||||
|
model: sidebarContainer.sidebarItems
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isActive: sidebarContainer.currentIndex === index
|
||||||
|
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 44
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: isActive ? Theme.primaryContainer : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: modelData.icon || ""
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: parent.parent.isActive ? Theme.surfaceText : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.text || ""
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: parent.parent.isActive ? Theme.surfaceText : Theme.surfaceText
|
||||||
|
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: tabMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
sidebarContainer.currentIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Settings
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: settingsModal
|
|
||||||
|
|
||||||
property Component settingsContent
|
|
||||||
|
|
||||||
signal closingModal
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible)
|
|
||||||
hide()
|
|
||||||
else
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
objectName: "settingsModal"
|
|
||||||
width: 800
|
|
||||||
height: 750
|
|
||||||
visible: false
|
|
||||||
onBackgroundClicked: hide()
|
|
||||||
content: settingsContent
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open() {
|
|
||||||
settingsModal.show()
|
|
||||||
return "SETTINGS_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
settingsModal.hide()
|
|
||||||
return "SETTINGS_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
settingsModal.toggle()
|
|
||||||
return "SETTINGS_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "settings"
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsContent: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
anchors.bottomMargin: Theme.spacingL
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 35
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
hoverColor: Theme.errorHover
|
|
||||||
onClicked: settingsModal.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 35
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: sidebarContainer
|
|
||||||
|
|
||||||
property int currentIndex: 0
|
|
||||||
|
|
||||||
width: 270
|
|
||||||
height: parent.height
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.topMargin: Theme.spacingM + 2
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 110
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: profileImageContainer
|
|
||||||
width: 80
|
|
||||||
height: 80
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
property bool hasImage: profileImageSource.status
|
|
||||||
=== Image.Ready
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 1
|
|
||||||
visible: parent.hasImage
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: profileImageSource
|
|
||||||
source: {
|
|
||||||
if (PortalService.profileImage === "")
|
|
||||||
return ""
|
|
||||||
if (PortalService.profileImage.startsWith(
|
|
||||||
"/"))
|
|
||||||
return "file://" + PortalService.profileImage
|
|
||||||
return PortalService.profileImage
|
|
||||||
}
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
mipmap: true
|
|
||||||
cache: true
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
source: profileImageSource
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: profileCircularMask
|
|
||||||
visible: profileImageContainer.hasImage
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: profileCircularMask
|
|
||||||
width: 70
|
|
||||||
height: 70
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: Theme.primary
|
|
||||||
visible: !parent.hasImage
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "person"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primaryText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "warning"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.error
|
|
||||||
visible: PortalService.profileImage !== ""
|
|
||||||
&& profileImageSource.status === Image.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.7)
|
|
||||||
visible: profileMouseArea.containsMouse
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: Qt.rgba(255, 255,
|
|
||||||
255, 0.9)
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "edit"
|
|
||||||
size: 16
|
|
||||||
color: "black"
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
settingsModal.allowFocusOverride = true
|
|
||||||
settingsModal.shouldHaveFocus = false
|
|
||||||
profileBrowser.open(
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: Qt.rgba(255, 255,
|
|
||||||
255, 0.9)
|
|
||||||
visible: profileImageContainer.hasImage
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "close"
|
|
||||||
size: 16
|
|
||||||
color: "black"
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
PortalService.setProfileImage(
|
|
||||||
"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: profileMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
propagateComposedEvents: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: 120
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: UserInfoService.fullName
|
|
||||||
|| "User"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.distribution
|
|
||||||
|| "Linux"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Theme.spacingL
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: sidebarRepeater
|
|
||||||
|
|
||||||
model: [{
|
|
||||||
"text": "Personalization",
|
|
||||||
"icon": "person"
|
|
||||||
}, {
|
|
||||||
"text": "Time & Date",
|
|
||||||
"icon": "schedule"
|
|
||||||
}, {
|
|
||||||
"text": "Weather",
|
|
||||||
"icon": "cloud"
|
|
||||||
}, {
|
|
||||||
"text": "Top Bar",
|
|
||||||
"icon": "toolbar"
|
|
||||||
}, {
|
|
||||||
"text": "Widgets",
|
|
||||||
"icon": "widgets"
|
|
||||||
}, {
|
|
||||||
"text": "Dock",
|
|
||||||
"icon": "dock_to_bottom"
|
|
||||||
}, {
|
|
||||||
"text": "Recent Apps",
|
|
||||||
"icon": "history"
|
|
||||||
}, {
|
|
||||||
"text": "Theme & Colors",
|
|
||||||
"icon": "palette"
|
|
||||||
}, {
|
|
||||||
"text": "About",
|
|
||||||
"icon": "info"
|
|
||||||
}]
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool isActive: sidebarContainer.currentIndex === index
|
|
||||||
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 44
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: isActive ? Theme.surfaceContainerHigh : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: modelData.icon || ""
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.text || ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: tabMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
sidebarContainer.currentIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - sidebarContainer.width
|
|
||||||
height: parent.height
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: 0
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: Theme.spacingM
|
|
||||||
anchors.topMargin: 0
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: personalizationLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 0
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
PersonalizationTab {
|
|
||||||
parentModal: settingsModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: timeLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 1
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: TimeTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: weatherLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 2
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: WeatherTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: topBarLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 3
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: TopBarTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: widgetsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 4
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: WidgetTweaksTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: dockLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 5
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: Component {
|
|
||||||
DockTab {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: recentAppsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 6
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: RecentAppsTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: themeColorsLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 7
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: ThemeColorsTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: aboutLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: sidebarContainer.currentIndex === 8
|
|
||||||
visible: active
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: AboutTab {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: profileBrowser
|
|
||||||
|
|
||||||
browserTitle: "Select Profile Image"
|
|
||||||
browserIcon: "person"
|
|
||||||
browserType: "profile"
|
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
onFileSelected: path => {
|
|
||||||
PortalService.setProfileImage(path)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
onDialogClosed: {
|
|
||||||
if (settingsModal) {
|
|
||||||
settingsModal.allowFocusOverride = false
|
|
||||||
settingsModal.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return settingsModal.shouldBeVisible
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
226
Modals/Spotlight/SpotlightContent.qml
Normal file
226
Modals/Spotlight/SpotlightContent.qml
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Spotlight
|
||||||
|
import qs.Modules.AppDrawer
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: spotlightKeyHandler
|
||||||
|
|
||||||
|
property alias appLauncher: appLauncher
|
||||||
|
property alias searchField: searchField
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Down) {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
|
||||||
|
searchField.forceActiveFocus()
|
||||||
|
searchField.insertText(event.text)
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLauncher {
|
||||||
|
id: appLauncher
|
||||||
|
|
||||||
|
viewMode: SettingsData.spotlightModalViewMode
|
||||||
|
gridColumns: 4
|
||||||
|
onAppLaunched: () => {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
}
|
||||||
|
onViewModeSelected: mode => {
|
||||||
|
SettingsData.setSpotlightModalViewMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: categorySelector.height + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceVariantAlpha
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0
|
||||||
|
|
||||||
|
CategorySelector {
|
||||||
|
id: categorySelector
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
categories: appLauncher.categories
|
||||||
|
selectedCategory: appLauncher.selectedCategory
|
||||||
|
compact: false
|
||||||
|
onCategorySelected: category => {
|
||||||
|
appLauncher.setCategory(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
width: parent.width - 80 - Theme.spacingM
|
||||||
|
height: 56
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
backgroundColor: Theme.surfaceContainerHigh
|
||||||
|
normalBorderColor: Theme.outlineMedium
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
leftIconName: "search"
|
||||||
|
leftIconSize: Theme.iconSize
|
||||||
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
showClearButton: true
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
enabled: parentModal ? parentModal.spotlightOpen : true
|
||||||
|
placeholderText: ""
|
||||||
|
ignoreLeftRightKeys: true
|
||||||
|
keyForwardTargets: [spotlightKeyHandler]
|
||||||
|
text: appLauncher.searchQuery
|
||||||
|
onTextEdited: () => {
|
||||||
|
appLauncher.searchQuery = text
|
||||||
|
}
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
|
||||||
|
event.accepted = true
|
||||||
|
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
||||||
|
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
else if (appLauncher.model.count > 0)
|
||||||
|
appLauncher.launchApp(appLauncher.model.get(0))
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||||
|
event.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: appLauncher.model.count > 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
border.color: appLauncher.viewMode === "list" ? Theme.primarySelected : "transparent"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "view_list"
|
||||||
|
size: 18
|
||||||
|
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listViewArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
appLauncher.setViewMode("list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
border.color: appLauncher.viewMode === "grid" ? Theme.primarySelected : "transparent"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "grid_view"
|
||||||
|
size: 18
|
||||||
|
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridViewArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
appLauncher.setViewMode("grid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotlightResults {
|
||||||
|
appLauncher: spotlightKeyHandler.appLauncher
|
||||||
|
contextMenu: contextMenu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotlightContextMenu {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
appLauncher: spotlightKeyHandler.appLauncher
|
||||||
|
parentHandler: spotlightKeyHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: contextMenu.visible
|
||||||
|
z: 999
|
||||||
|
onClicked: () => {
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
|
||||||
|
// Prevent closing when clicking on the menu itself
|
||||||
|
x: contextMenu.x
|
||||||
|
y: contextMenu.y
|
||||||
|
width: contextMenu.width
|
||||||
|
height: contextMenu.height
|
||||||
|
onClicked: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
Modals/Spotlight/SpotlightContextMenu.qml
Normal file
205
Modals/Spotlight/SpotlightContextMenu.qml
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
property var currentApp: null
|
||||||
|
property bool menuVisible: false
|
||||||
|
property var appLauncher: null
|
||||||
|
property var parentHandler: null
|
||||||
|
|
||||||
|
function show(x, y, app) {
|
||||||
|
currentApp = app
|
||||||
|
const menuWidth = 180
|
||||||
|
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
let finalX = x + 8
|
||||||
|
let finalY = y + 8
|
||||||
|
if (parentHandler) {
|
||||||
|
if (finalX + menuWidth > parentHandler.width)
|
||||||
|
finalX = x - menuWidth - 8
|
||||||
|
|
||||||
|
if (finalY + menuHeight > parentHandler.height)
|
||||||
|
finalY = y - menuHeight - 8
|
||||||
|
|
||||||
|
finalX = Math.max(8, Math.min(finalX, parentHandler.width - menuWidth - 8))
|
||||||
|
finalY = Math.max(8, Math.min(finalY, parentHandler.height - menuHeight - 8))
|
||||||
|
}
|
||||||
|
contextMenu.x = finalX
|
||||||
|
contextMenu.y = finalY
|
||||||
|
contextMenu.visible = true
|
||||||
|
contextMenu.menuVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
contextMenu.menuVisible = false
|
||||||
|
Qt.callLater(() => {
|
||||||
|
contextMenu.visible = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
width: 180
|
||||||
|
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
z: 1000
|
||||||
|
opacity: menuVisible ? 1 : 0
|
||||||
|
scale: menuVisible ? 1 : 0.85
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 4
|
||||||
|
anchors.leftMargin: 2
|
||||||
|
anchors.rightMargin: -2
|
||||||
|
anchors.bottomMargin: -4
|
||||||
|
radius: parent.radius
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.15)
|
||||||
|
z: parent.z - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: menuColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return "push_pin"
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return "Pin to Dock"
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
return SessionData.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pinMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
if (SessionData.isPinnedApp(appId))
|
||||||
|
SessionData.removePinnedApp(appId)
|
||||||
|
else
|
||||||
|
SessionData.addPinnedApp(appId)
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 5
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "launch"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Launch"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: launchMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
if (contextMenu.currentApp && appLauncher)
|
||||||
|
appLauncher.launchApp(contextMenu.currentApp)
|
||||||
|
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
Modals/Spotlight/SpotlightModal.qml
Normal file
109
Modals/Spotlight/SpotlightModal.qml
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modules.AppDrawer
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: spotlightModal
|
||||||
|
|
||||||
|
property bool spotlightOpen: false
|
||||||
|
property Component spotlightContent
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
spotlightOpen = true
|
||||||
|
open()
|
||||||
|
if (contentLoader.item && contentLoader.item.appLauncher) {
|
||||||
|
contentLoader.item.appLauncher.searchQuery = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
spotlightOpen = false
|
||||||
|
close()
|
||||||
|
if (contentLoader.item && contentLoader.item.appLauncher) {
|
||||||
|
contentLoader.item.appLauncher.searchQuery = ""
|
||||||
|
contentLoader.item.appLauncher.selectedIndex = 0
|
||||||
|
contentLoader.item.appLauncher.setCategory("All")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (spotlightOpen) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: spotlightOpen
|
||||||
|
width: 550
|
||||||
|
height: 600
|
||||||
|
backgroundColor: Theme.popupBackground()
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
borderColor: Theme.outlineMedium
|
||||||
|
borderWidth: 1
|
||||||
|
enableShadow: true
|
||||||
|
onVisibleChanged: () => {
|
||||||
|
if (visible && !spotlightOpen) {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
if (visible && contentLoader.item) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return hide()
|
||||||
|
}
|
||||||
|
content: spotlightContent
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCloseAllModalsExcept(excludedModal) {
|
||||||
|
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
|
||||||
|
spotlightOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: ModalManager
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
spotlightModal.show()
|
||||||
|
return "SPOTLIGHT_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
spotlightModal.hide()
|
||||||
|
return "SPOTLIGHT_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
spotlightModal.toggle()
|
||||||
|
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "spotlight"
|
||||||
|
}
|
||||||
|
|
||||||
|
spotlightContent: Component {
|
||||||
|
SpotlightContent {
|
||||||
|
parentModal: spotlightModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
324
Modals/Spotlight/SpotlightResults.qml
Normal file
324
Modals/Spotlight/SpotlightResults.qml
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: resultsContainer
|
||||||
|
|
||||||
|
property var appLauncher: null
|
||||||
|
property var contextMenu: null
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - y
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: resultsList
|
||||||
|
|
||||||
|
property int itemHeight: 60
|
||||||
|
property int iconSize: 40
|
||||||
|
property bool showDescription: true
|
||||||
|
property int itemSpacing: Theme.spacingS
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
const itemY = index * (itemHeight + itemSpacing)
|
||||||
|
const itemBottom = itemY + itemHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
visible: appLauncher && appLauncher.viewMode === "list"
|
||||||
|
model: appLauncher ? appLauncher.model : null
|
||||||
|
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||||
|
clip: true
|
||||||
|
spacing: itemSpacing
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
onItemClicked: (index, modelData) => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||||
|
if (contextMenu)
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: () => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: resultsList.itemHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent"
|
||||||
|
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: ListView.isCurrentItem ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: resultsList.iconSize
|
||||||
|
height: resultsList.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: listIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, true)
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !listIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: resultsList.iconSize * 0.4
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - resultsList.iconSize - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.comment || "Application"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
visible: resultsList.showDescription && model.comment && model.comment.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: () => {
|
||||||
|
if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive)
|
||||||
|
resultsList.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: () => {
|
||||||
|
resultsList.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
resultsList.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
|
||||||
|
resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: resultsGrid
|
||||||
|
|
||||||
|
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||||
|
property int columns: 4
|
||||||
|
property bool adaptiveColumns: false
|
||||||
|
property int minCellWidth: 120
|
||||||
|
property int maxCellWidth: 160
|
||||||
|
property int cellPadding: 8
|
||||||
|
property real iconSizeRatio: 0.55
|
||||||
|
property int maxIconSize: 48
|
||||||
|
property int minIconSize: 32
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||||
|
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||||
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
|
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||||
|
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
const itemY = Math.floor(index / actualColumns) * cellHeight
|
||||||
|
const itemBottom = itemY + cellHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
visible: appLauncher && appLauncher.viewMode === "grid"
|
||||||
|
model: appLauncher ? appLauncher.model : null
|
||||||
|
clip: true
|
||||||
|
cellWidth: baseCellWidth
|
||||||
|
cellHeight: baseCellHeight
|
||||||
|
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||||
|
rightMargin: leftMargin
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
onItemClicked: (index, modelData) => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||||
|
if (contextMenu)
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: () => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: resultsGrid.cellWidth - resultsGrid.cellPadding
|
||||||
|
height: resultsGrid.cellHeight - resultsGrid.cellPadding
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent"
|
||||||
|
border.color: resultsGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: resultsGrid.currentIndex === index ? 2 : 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio))
|
||||||
|
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: gridIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, true)
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !gridIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: resultsGrid.cellWidth - 12
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: () => {
|
||||||
|
if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive)
|
||||||
|
resultsGrid.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: () => {
|
||||||
|
resultsGrid.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
resultsGrid.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
|
||||||
|
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,919 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.AppDrawer
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: spotlightModal
|
|
||||||
|
|
||||||
property bool spotlightOpen: false
|
|
||||||
property Component spotlightContent
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
spotlightOpen = true
|
|
||||||
open()
|
|
||||||
if (contentLoader.item && contentLoader.item.appLauncher)
|
|
||||||
contentLoader.item.appLauncher.searchQuery = ""
|
|
||||||
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item && contentLoader.item.searchField)
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
spotlightOpen = false
|
|
||||||
close()
|
|
||||||
if (contentLoader.item && contentLoader.item.appLauncher) {
|
|
||||||
contentLoader.item.appLauncher.searchQuery = ""
|
|
||||||
contentLoader.item.appLauncher.selectedIndex = 0
|
|
||||||
contentLoader.item.appLauncher.setCategory("All")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (spotlightOpen)
|
|
||||||
hide()
|
|
||||||
else
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: spotlightOpen
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: ModalManager
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== spotlightModal && !allowStacking
|
|
||||||
&& spotlightOpen) {
|
|
||||||
spotlightOpen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width: 550
|
|
||||||
height: 600
|
|
||||||
backgroundColor: Theme.popupBackground()
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
borderColor: Theme.outlineMedium
|
|
||||||
borderWidth: 1
|
|
||||||
enableShadow: true
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && !spotlightOpen)
|
|
||||||
show()
|
|
||||||
|
|
||||||
if (visible && contentLoader.item)
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (contentLoader.item.searchField)
|
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onBackgroundClicked: {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
|
|
||||||
}
|
|
||||||
content: spotlightContent
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open() {
|
|
||||||
spotlightModal.show()
|
|
||||||
return "SPOTLIGHT_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
spotlightModal.hide()
|
|
||||||
return "SPOTLIGHT_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
spotlightModal.toggle()
|
|
||||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "spotlight"
|
|
||||||
}
|
|
||||||
|
|
||||||
spotlightContent: Component {
|
|
||||||
Item {
|
|
||||||
id: spotlightKeyHandler
|
|
||||||
|
|
||||||
property alias appLauncher: appLauncher
|
|
||||||
property alias searchField: searchField
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
appLauncher.selectNext()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
appLauncher.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Right
|
|
||||||
&& appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Left
|
|
||||||
&& appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Return
|
|
||||||
|| event.key === Qt.Key_Enter) {
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (!searchField.activeFocus && event.text
|
|
||||||
&& event.text.length > 0 && event.text.match(
|
|
||||||
/[a-zA-Z0-9\\s]/)) {
|
|
||||||
searchField.forceActiveFocus()
|
|
||||||
searchField.insertText(event.text)
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppLauncher {
|
|
||||||
id: appLauncher
|
|
||||||
|
|
||||||
viewMode: SettingsData.spotlightModalViewMode
|
|
||||||
gridColumns: 4
|
|
||||||
onAppLaunched: hide()
|
|
||||||
onViewModeSelected: function (mode) {
|
|
||||||
SettingsData.setSpotlightModalViewMode(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: categorySelector.height + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceVariantAlpha
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
visible: appLauncher.categories.length > 1
|
|
||||||
|| appLauncher.model.count > 0
|
|
||||||
|
|
||||||
CategorySelector {
|
|
||||||
id: categorySelector
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
categories: appLauncher.categories
|
|
||||||
selectedCategory: appLauncher.selectedCategory
|
|
||||||
compact: false
|
|
||||||
onCategorySelected: category => {
|
|
||||||
return appLauncher.setCategory(
|
|
||||||
category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
|
|
||||||
width: parent.width - 80
|
|
||||||
- Theme.spacingM // Leave space for view toggle buttons
|
|
||||||
height: 56
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
backgroundColor: Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha(
|
|
||||||
) * 0.7)
|
|
||||||
normalBorderColor: Theme.outlineMedium
|
|
||||||
focusedBorderColor: Theme.primary
|
|
||||||
leftIconName: "search"
|
|
||||||
leftIconSize: Theme.iconSize
|
|
||||||
leftIconColor: Theme.surfaceVariantText
|
|
||||||
leftIconFocusedColor: Theme.primary
|
|
||||||
showClearButton: true
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
enabled: spotlightOpen
|
|
||||||
placeholderText: ""
|
|
||||||
ignoreLeftRightKeys: true
|
|
||||||
keyForwardTargets: [spotlightKeyHandler]
|
|
||||||
text: appLauncher.searchQuery
|
|
||||||
onTextEdited: {
|
|
||||||
appLauncher.searchQuery = text
|
|
||||||
}
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
hide()
|
|
||||||
event.accepted = true
|
|
||||||
} else if ((event.key === Qt.Key_Return
|
|
||||||
|| event.key === Qt.Key_Enter)
|
|
||||||
&& text.length > 0) {
|
|
||||||
if (appLauncher.keyboardNavigationActive
|
|
||||||
&& appLauncher.model.count > 0)
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
else if (appLauncher.model.count > 0)
|
|
||||||
appLauncher.launchApp(
|
|
||||||
appLauncher.model.get(0))
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: appLauncher.model.count > 0
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
border.color: appLauncher.viewMode
|
|
||||||
=== "list" ? Theme.primarySelected : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "view_list"
|
|
||||||
size: 18
|
|
||||||
color: appLauncher.viewMode
|
|
||||||
=== "list" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: listViewArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
appLauncher.setViewMode("list")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
border.color: appLauncher.viewMode
|
|
||||||
=== "grid" ? Theme.primarySelected : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "grid_view"
|
|
||||||
size: 18
|
|
||||||
color: appLauncher.viewMode
|
|
||||||
=== "grid" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gridViewArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
appLauncher.setViewMode("grid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: resultsContainer
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - y
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: resultsList
|
|
||||||
|
|
||||||
property int itemHeight: 60
|
|
||||||
property int iconSize: 40
|
|
||||||
property bool showDescription: true
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
var itemY = index * (itemHeight + itemSpacing)
|
|
||||||
var itemBottom = itemY + itemHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
visible: appLauncher.viewMode === "list"
|
|
||||||
model: appLauncher.model
|
|
||||||
currentIndex: appLauncher.selectedIndex
|
|
||||||
clip: true
|
|
||||||
spacing: itemSpacing
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
onItemClicked: function (index, modelData) {
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOn
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: ListView.view.width
|
|
||||||
height: resultsList.itemHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
|
||||||
border.width: ListView.isCurrentItem ? 2 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: resultsList.iconSize
|
|
||||||
height: resultsList.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: listIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: (model.icon) ? Quickshell.iconPath(
|
|
||||||
model.icon,
|
|
||||||
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !listIconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name
|
|
||||||
&& model.name.length
|
|
||||||
> 0) ? model.name.charAt(
|
|
||||||
0).toUpperCase(
|
|
||||||
) : "A"
|
|
||||||
font.pixelSize: resultsList.iconSize * 0.4
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - resultsList.iconSize - Theme.spacingL
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.comment || "Application"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
visible: resultsList.showDescription
|
|
||||||
&& model.comment
|
|
||||||
&& model.comment.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: listMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
z: 10
|
|
||||||
onEntered: {
|
|
||||||
if (resultsList.hoverUpdatesSelection
|
|
||||||
&& !resultsList.keyboardNavigationActive)
|
|
||||||
resultsList.currentIndex = index
|
|
||||||
}
|
|
||||||
onPositionChanged: {
|
|
||||||
resultsList.keyboardNavigationReset()
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
resultsList.itemClicked(
|
|
||||||
index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
var modalPos = mapToItem(
|
|
||||||
spotlightKeyHandler,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
resultsList.itemRightClicked(
|
|
||||||
index, model,
|
|
||||||
modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: resultsGrid
|
|
||||||
|
|
||||||
property int currentIndex: appLauncher.selectedIndex
|
|
||||||
property int columns: 4
|
|
||||||
property bool adaptiveColumns: false
|
|
||||||
property int minCellWidth: 120
|
|
||||||
property int maxCellWidth: 160
|
|
||||||
property int cellPadding: 8
|
|
||||||
property real iconSizeRatio: 0.55
|
|
||||||
property int maxIconSize: 48
|
|
||||||
property int minIconSize: 32
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
|
||||||
property int baseCellWidth: adaptiveColumns ? Math.max(
|
|
||||||
minCellWidth,
|
|
||||||
Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
|
||||||
property int baseCellHeight: baseCellWidth + 20
|
|
||||||
property int actualColumns: adaptiveColumns ? Math.floor(
|
|
||||||
width
|
|
||||||
/ cellWidth) : columns
|
|
||||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
var itemY = Math.floor(
|
|
||||||
index / actualColumns) * cellHeight
|
|
||||||
var itemBottom = itemY + cellHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
visible: appLauncher.viewMode === "grid"
|
|
||||||
model: appLauncher.model
|
|
||||||
clip: true
|
|
||||||
cellWidth: baseCellWidth
|
|
||||||
cellHeight: baseCellHeight
|
|
||||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
|
||||||
rightMargin: leftMargin
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
onItemClicked: function (index, modelData) {
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: resultsGrid.cellWidth - resultsGrid.cellPadding
|
|
||||||
height: resultsGrid.cellHeight - resultsGrid.cellPadding
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: resultsGrid.currentIndex
|
|
||||||
=== index ? Theme.primarySelected : Theme.outlineMedium
|
|
||||||
border.width: resultsGrid.currentIndex === index ? 2 : 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int iconSize: Math.min(
|
|
||||||
resultsGrid.maxIconSize,
|
|
||||||
Math.max(
|
|
||||||
resultsGrid.minIconSize,
|
|
||||||
resultsGrid.cellWidth
|
|
||||||
* resultsGrid.iconSizeRatio))
|
|
||||||
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: gridIconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: (model.icon) ? Quickshell.iconPath(
|
|
||||||
model.icon,
|
|
||||||
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !gridIconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name
|
|
||||||
&& model.name.length
|
|
||||||
> 0) ? model.name.charAt(
|
|
||||||
0).toUpperCase(
|
|
||||||
) : "A"
|
|
||||||
font.pixelSize: Math.min(
|
|
||||||
28,
|
|
||||||
parent.width * 0.5)
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
width: resultsGrid.cellWidth - 12
|
|
||||||
text: model.name || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
maximumLineCount: 2
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gridMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
z: 10
|
|
||||||
onEntered: {
|
|
||||||
if (resultsGrid.hoverUpdatesSelection
|
|
||||||
&& !resultsGrid.keyboardNavigationActive)
|
|
||||||
resultsGrid.currentIndex = index
|
|
||||||
}
|
|
||||||
onPositionChanged: {
|
|
||||||
resultsGrid.keyboardNavigationReset()
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
resultsGrid.itemClicked(
|
|
||||||
index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
var modalPos = mapToItem(
|
|
||||||
spotlightKeyHandler,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
resultsGrid.itemRightClicked(
|
|
||||||
index, model,
|
|
||||||
modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
property var currentApp: null
|
|
||||||
property bool menuVisible: false
|
|
||||||
|
|
||||||
function show(x, y, app) {
|
|
||||||
currentApp = app
|
|
||||||
|
|
||||||
const menuWidth = 180
|
|
||||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
let finalX = x + 8
|
|
||||||
let finalY = y + 8
|
|
||||||
|
|
||||||
if (finalX + menuWidth > spotlightKeyHandler.width) {
|
|
||||||
finalX = x - menuWidth - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalY + menuHeight > spotlightKeyHandler.height) {
|
|
||||||
finalY = y - menuHeight - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
finalX = Math.max(
|
|
||||||
8, Math.min(
|
|
||||||
finalX,
|
|
||||||
spotlightKeyHandler.width - menuWidth - 8))
|
|
||||||
finalY = Math.max(
|
|
||||||
8, Math.min(
|
|
||||||
finalY,
|
|
||||||
spotlightKeyHandler.height - menuHeight - 8))
|
|
||||||
|
|
||||||
contextMenu.x = finalX
|
|
||||||
contextMenu.y = finalY
|
|
||||||
contextMenu.visible = true
|
|
||||||
contextMenu.menuVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
contextMenu.menuVisible = false
|
|
||||||
Qt.callLater(() => {
|
|
||||||
contextMenu.visible = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 180
|
|
||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
opacity: menuVisible ? 1 : 0
|
|
||||||
scale: menuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: pinMouseArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "push_pin"
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
return SessionData.isPinnedApp(
|
|
||||||
appId) ? "keep_off" : "push_pin"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "Pin to Dock"
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
return SessionData.isPinnedApp(
|
|
||||||
appId) ? "Unpin from Dock" : "Pin to Dock"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: pinMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
if (SessionData.isPinnedApp(appId))
|
|
||||||
SessionData.removePinnedApp(appId)
|
|
||||||
else
|
|
||||||
SessionData.addPinnedApp(appId)
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: launchMouseArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "launch"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Launch"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: launchMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (contextMenu.currentApp)
|
|
||||||
appLauncher.launchApp(
|
|
||||||
contextMenu.currentApp)
|
|
||||||
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: contextMenu.visible
|
|
||||||
z: 999
|
|
||||||
onClicked: {
|
|
||||||
contextMenu.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: contextMenu.x
|
|
||||||
y: contextMenu.y
|
|
||||||
width: contextMenu.width
|
|
||||||
height: contextMenu.height
|
|
||||||
onClicked: {
|
|
||||||
|
|
||||||
// Prevent closing when clicking on the menu itself
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -14,58 +14,56 @@ DankModal {
|
|||||||
wifiPasswordSSID = ssid
|
wifiPasswordSSID = ssid
|
||||||
wifiPasswordInput = ""
|
wifiPasswordInput = ""
|
||||||
open()
|
open()
|
||||||
Qt.callLater(function () {
|
Qt.callLater(() => {
|
||||||
if (contentLoader.item && contentLoader.item.passwordInput) {
|
if (contentLoader.item && contentLoader.item.passwordInput)
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
contentLoader.item.passwordInput.forceActiveFocus()
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldBeVisible: false
|
shouldBeVisible: false
|
||||||
width: 420
|
width: 420
|
||||||
height: 230
|
height: 230
|
||||||
onShouldBeVisibleChanged: {
|
onShouldBeVisibleChanged: () => {
|
||||||
if (!shouldBeVisible)
|
if (!shouldBeVisible)
|
||||||
wifiPasswordInput = ""
|
wifiPasswordInput = ""
|
||||||
}
|
}
|
||||||
onOpened: {
|
onOpened: {
|
||||||
Qt.callLater(function () {
|
Qt.callLater(() => {
|
||||||
if (contentLoader.item && contentLoader.item.passwordInput) {
|
if (contentLoader.item && contentLoader.item.passwordInput)
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
contentLoader.item.passwordInput.forceActiveFocus()
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
onBackgroundClicked: {
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
}
|
}
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
target: NetworkService
|
||||||
|
|
||||||
function onPasswordDialogShouldReopenChanged() {
|
function onPasswordDialogShouldReopenChanged() {
|
||||||
if (NetworkService.passwordDialogShouldReopen
|
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
|
||||||
&& NetworkService.connectingSSID !== "") {
|
|
||||||
wifiPasswordSSID = NetworkService.connectingSSID
|
wifiPasswordSSID = NetworkService.connectingSSID
|
||||||
wifiPasswordInput = ""
|
wifiPasswordInput = ""
|
||||||
open()
|
open()
|
||||||
NetworkService.passwordDialogShouldReopen = false
|
NetworkService.passwordDialogShouldReopen = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target: NetworkService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: wifiContent
|
id: wifiContent
|
||||||
|
|
||||||
property alias passwordInput: passwordInput
|
property alias passwordInput: passwordInput
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
Keys.onEscapePressed: function (event) {
|
Keys.onEscapePressed: event => {
|
||||||
close()
|
close()
|
||||||
wifiPasswordInput = ""
|
wifiPasswordInput = ""
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -87,7 +85,7 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: "Enter password for \"" + wifiPasswordSSID + "\""
|
text: `Enter password for "${wifiPasswordSSID}"`
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -99,11 +97,10 @@ DankModal {
|
|||||||
iconName: "close"
|
iconName: "close"
|
||||||
iconSize: Theme.iconSize - 4
|
iconSize: Theme.iconSize - 4
|
||||||
iconColor: Theme.surfaceText
|
iconColor: Theme.surfaceText
|
||||||
hoverColor: Theme.errorHover
|
onClicked: () => {
|
||||||
onClicked: {
|
close()
|
||||||
close()
|
wifiPasswordInput = ""
|
||||||
wifiPasswordInput = ""
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +114,9 @@ DankModal {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: {
|
onClicked: () => {
|
||||||
passwordInput.forceActiveFocus()
|
passwordInput.forceActiveFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankTextField {
|
DankTextField {
|
||||||
@@ -134,40 +131,37 @@ DankModal {
|
|||||||
backgroundColor: "transparent"
|
backgroundColor: "transparent"
|
||||||
focus: true
|
focus: true
|
||||||
enabled: root.shouldBeVisible
|
enabled: root.shouldBeVisible
|
||||||
onTextEdited: {
|
onTextEdited: () => {
|
||||||
wifiPasswordInput = text
|
wifiPasswordInput = text
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: () => {
|
||||||
NetworkService.connectToWifiWithPassword(
|
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
||||||
wifiPasswordSSID, passwordInput.text)
|
close()
|
||||||
close()
|
wifiPasswordInput = ""
|
||||||
wifiPasswordInput = ""
|
passwordInput.text = ""
|
||||||
passwordInput.text = ""
|
}
|
||||||
}
|
Component.onCompleted: () => {
|
||||||
|
if (root.shouldBeVisible)
|
||||||
Component.onCompleted: {
|
focusDelayTimer.start()
|
||||||
if (root.shouldBeVisible) {
|
}
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: focusDelayTimer
|
id: focusDelayTimer
|
||||||
|
|
||||||
interval: 100
|
interval: 100
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: () => {
|
||||||
if (root.shouldBeVisible) {
|
if (root.shouldBeVisible)
|
||||||
passwordInput.forceActiveFocus()
|
passwordInput.forceActiveFocus()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
|
|
||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
if (root.shouldBeVisible) {
|
if (root.shouldBeVisible)
|
||||||
focusDelayTimer.start()
|
focusDelayTimer.start()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,9 +194,9 @@ DankModal {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: () => {
|
||||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,9 +218,7 @@ DankModal {
|
|||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: Math.max(
|
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||||
70,
|
|
||||||
cancelText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
height: 36
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||||
@@ -249,22 +241,18 @@ DankModal {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: () => {
|
||||||
close()
|
close()
|
||||||
wifiPasswordInput = ""
|
wifiPasswordInput = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: Math.max(
|
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||||
80,
|
|
||||||
connectText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
height: 36
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: connectArea.containsMouse ? Qt.darker(
|
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
Theme.primary,
|
|
||||||
1.1) : Theme.primary
|
|
||||||
enabled: passwordInput.text.length > 0
|
enabled: passwordInput.text.length > 0
|
||||||
opacity: enabled ? 1 : 0.5
|
opacity: enabled ? 1 : 0.5
|
||||||
|
|
||||||
@@ -285,14 +273,12 @@ DankModal {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
enabled: parent.enabled
|
enabled: parent.enabled
|
||||||
onClicked: {
|
onClicked: () => {
|
||||||
NetworkService.connectToWifiWithPassword(
|
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
||||||
wifiPasswordSSID,
|
close()
|
||||||
passwordInput.text)
|
wifiPasswordInput = ""
|
||||||
close()
|
passwordInput.text = ""
|
||||||
wifiPasswordInput = ""
|
}
|
||||||
passwordInput.text = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
@@ -16,16 +15,11 @@ DankPopout {
|
|||||||
property string triggerSection: "left"
|
property string triggerSection: "left"
|
||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
|
|
||||||
|
// Setting to Exclusive, so virtual keyboards can send input to app drawer
|
||||||
|
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
open()
|
open()
|
||||||
appLauncher.searchQuery = ""
|
|
||||||
appLauncher.selectedIndex = 0
|
|
||||||
appLauncher.setCategory("All")
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
function setTriggerPosition(x, y, width, section, screen) {
|
||||||
@@ -42,15 +36,20 @@ DankPopout {
|
|||||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
||||||
triggerWidth: 40
|
triggerWidth: 40
|
||||||
positioning: "center"
|
positioning: "center"
|
||||||
WlrLayershell.namespace: "quickshell-launcher"
|
|
||||||
screen: triggerScreen
|
screen: triggerScreen
|
||||||
|
|
||||||
onOpened: {
|
onShouldBeVisibleChanged: {
|
||||||
Qt.callLater(() => {
|
if (shouldBeVisible) {
|
||||||
if (contentLoader.item && contentLoader.item.searchField) {
|
appLauncher.searchQuery = ""
|
||||||
contentLoader.item.searchField.forceActiveFocus()
|
appLauncher.selectedIndex = 0
|
||||||
}
|
appLauncher.setCategory("All")
|
||||||
})
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.text = ""
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppLauncher {
|
AppLauncher {
|
||||||
@@ -58,7 +57,7 @@ DankPopout {
|
|||||||
|
|
||||||
viewMode: SettingsData.appLauncherViewMode
|
viewMode: SettingsData.appLauncherViewMode
|
||||||
gridColumns: 4
|
gridColumns: 4
|
||||||
onAppLaunched: appDrawerPopout.hide()
|
onAppLaunched: appDrawerPopout.close()
|
||||||
onViewModeSelected: function (mode) {
|
onViewModeSelected: function (mode) {
|
||||||
SettingsData.setAppLauncherViewMode(mode)
|
SettingsData.setAppLauncherViewMode(mode)
|
||||||
}
|
}
|
||||||
@@ -75,34 +74,30 @@ DankPopout {
|
|||||||
antialiasing: true
|
antialiasing: true
|
||||||
smooth: true
|
smooth: true
|
||||||
|
|
||||||
Rectangle {
|
// Multi-layer border effect
|
||||||
anchors.fill: parent
|
Repeater {
|
||||||
anchors.margins: -3
|
model: [{
|
||||||
color: "transparent"
|
"margin": -3,
|
||||||
radius: parent.radius + 3
|
"color": Qt.rgba(0, 0, 0, 0.05),
|
||||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
"z": -3
|
||||||
border.width: 1
|
}, {
|
||||||
z: -3
|
"margin": -2,
|
||||||
}
|
"color": Qt.rgba(0, 0, 0, 0.08),
|
||||||
|
"z": -2
|
||||||
Rectangle {
|
}, {
|
||||||
anchors.fill: parent
|
"margin": 0,
|
||||||
anchors.margins: -2
|
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
|
||||||
color: "transparent"
|
"z": -1
|
||||||
radius: parent.radius + 2
|
}]
|
||||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
Rectangle {
|
||||||
border.width: 1
|
anchors.fill: parent
|
||||||
z: -2
|
anchors.margins: modelData.margin
|
||||||
}
|
color: "transparent"
|
||||||
|
radius: parent.radius + Math.abs(modelData.margin)
|
||||||
Rectangle {
|
border.color: modelData.color
|
||||||
anchors.fill: parent
|
border.width: 1
|
||||||
color: "transparent"
|
z: modelData.z
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
}
|
||||||
Theme.outline.b, 0.12)
|
|
||||||
border.width: 1
|
|
||||||
radius: parent.radius
|
|
||||||
z: -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -110,31 +105,30 @@ DankPopout {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
|
readonly property var keyMappings: {
|
||||||
|
const mappings = {}
|
||||||
|
mappings[Qt.Key_Escape] = () => appDrawerPopout.close()
|
||||||
|
mappings[Qt.Key_Down] = () => appLauncher.selectNext()
|
||||||
|
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious()
|
||||||
|
mappings[Qt.Key_Return] = () => appLauncher.launchSelected()
|
||||||
|
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected()
|
||||||
|
|
||||||
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow()
|
||||||
|
mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow()
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onPressed: function (event) {
|
Keys.onPressed: function (event) {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (keyMappings[event.key]) {
|
||||||
appDrawerPopout.close()
|
keyMappings[event.key]()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Down) {
|
return
|
||||||
appLauncher.selectNext()
|
}
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
if (!searchField.activeFocus && event.text && /[a-zA-Z0-9\s]/.test(event.text)) {
|
||||||
appLauncher.selectPrevious()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Right
|
|
||||||
&& appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Left
|
|
||||||
&& appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Return
|
|
||||||
|| event.key === Qt.Key_Enter) {
|
|
||||||
appLauncher.launchSelected()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (!searchField.activeFocus && event.text
|
|
||||||
&& event.text.length > 0 && event.text.match(
|
|
||||||
/[a-zA-Z0-9\\s]/)) {
|
|
||||||
searchField.forceActiveFocus()
|
searchField.forceActiveFocus()
|
||||||
searchField.insertText(event.text)
|
searchField.insertText(event.text)
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
@@ -179,15 +173,8 @@ DankPopout {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 52
|
height: 52
|
||||||
cornerRadius: Theme.cornerRadius
|
cornerRadius: Theme.cornerRadius
|
||||||
backgroundColor: Qt.rgba(
|
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
|
||||||
Theme.surfaceVariant.r,
|
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha(
|
|
||||||
) * 0.7)
|
|
||||||
normalBorderColor: Qt.rgba(Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
focusedBorderColor: Theme.primary
|
focusedBorderColor: Theme.primary
|
||||||
leftIconName: "search"
|
leftIconName: "search"
|
||||||
leftIconSize: Theme.iconSize
|
leftIconSize: Theme.iconSize
|
||||||
@@ -205,28 +192,34 @@ DankPopout {
|
|||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
appDrawerPopout.close()
|
appDrawerPopout.close()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if ((event.key === Qt.Key_Return
|
return
|
||||||
|| event.key === Qt.Key_Enter)
|
}
|
||||||
&& text.length > 0) {
|
|
||||||
if (appLauncher.keyboardNavigationActive
|
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
|
||||||
&& appLauncher.model.count > 0) {
|
const hasText = text.length > 0
|
||||||
|
|
||||||
|
if (isEnterKey && hasText) {
|
||||||
|
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
|
||||||
appLauncher.launchSelected()
|
appLauncher.launchSelected()
|
||||||
} else if (appLauncher.model.count > 0) {
|
} else if (appLauncher.model.count > 0) {
|
||||||
var firstApp = appLauncher.model.get(0)
|
appLauncher.launchApp(appLauncher.model.get(0))
|
||||||
appLauncher.launchApp(firstApp)
|
|
||||||
}
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Down || event.key
|
return
|
||||||
=== Qt.Key_Up || event.key === Qt.Key_Left || event.key
|
|
||||||
=== Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
|
||||||
event.accepted = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right]
|
||||||
|
const isNavigationKey = navigationKeys.includes(event.key)
|
||||||
|
const isEmptyEnter = isEnterKey && !hasText
|
||||||
|
|
||||||
|
event.accepted = !(isNavigationKey || isEmptyEnter)
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
if (!appDrawerPopout.shouldBeVisible)
|
if (!appDrawerPopout.shouldBeVisible) {
|
||||||
searchField.clearFocus()
|
searchField.focus = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target: appDrawerPopout
|
target: appDrawerPopout
|
||||||
@@ -269,24 +262,8 @@ DankPopout {
|
|||||||
circular: false
|
circular: false
|
||||||
iconName: "view_list"
|
iconName: "view_list"
|
||||||
iconSize: 20
|
iconSize: 20
|
||||||
iconColor: appLauncher.viewMode
|
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||||
=== "list" ? Theme.primary : Theme.surfaceText
|
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
hoverColor: appLauncher.viewMode
|
|
||||||
=== "list" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
backgroundColor: appLauncher.viewMode
|
|
||||||
=== "list" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
appLauncher.setViewMode("list")
|
appLauncher.setViewMode("list")
|
||||||
}
|
}
|
||||||
@@ -297,24 +274,8 @@ DankPopout {
|
|||||||
circular: false
|
circular: false
|
||||||
iconName: "grid_view"
|
iconName: "grid_view"
|
||||||
iconSize: 20
|
iconSize: 20
|
||||||
iconColor: appLauncher.viewMode
|
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||||
=== "grid" ? Theme.primary : Theme.surfaceText
|
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
hoverColor: appLauncher.viewMode
|
|
||||||
=== "grid" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
backgroundColor: appLauncher.viewMode
|
|
||||||
=== "grid" ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
appLauncher.setViewMode("grid")
|
appLauncher.setViewMode("grid")
|
||||||
}
|
}
|
||||||
@@ -331,11 +292,8 @@ DankPopout {
|
|||||||
return parent.height - usedHeight
|
return parent.height - usedHeight
|
||||||
}
|
}
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
|
||||||
Theme.surfaceVariant.g,
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||||
Theme.surfaceVariant.b, 0.1)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.05)
|
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
DankListView {
|
DankListView {
|
||||||
@@ -391,14 +349,6 @@ DankPopout {
|
|||||||
appLauncher.keyboardNavigationActive = false
|
appLauncher.keyboardNavigationActive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOn
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: ListView.view.width
|
width: ListView.view.width
|
||||||
height: appList.itemHeight
|
height: appList.itemHeight
|
||||||
@@ -421,9 +371,7 @@ DankPopout {
|
|||||||
id: listIconImg
|
id: listIconImg
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: (model.icon) ? Quickshell.iconPath(
|
source: Quickshell.iconPath(model.icon, true)
|
||||||
model.icon,
|
|
||||||
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: status === Image.Ready
|
visible: status === Image.Ready
|
||||||
@@ -439,11 +387,7 @@ DankPopout {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: (model.name
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
&& model.name.length
|
|
||||||
> 0) ? model.name.charAt(
|
|
||||||
0).toUpperCase(
|
|
||||||
) : "A"
|
|
||||||
font.pixelSize: appList.iconSize * 0.4
|
font.pixelSize: appList.iconSize * 0.4
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
@@ -471,9 +415,8 @@ DankPopout {
|
|||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
visible: appList.showDescription
|
maximumLineCount: 1
|
||||||
&& model.comment
|
visible: appList.showDescription && model.comment && model.comment.length > 0
|
||||||
&& model.comment.length > 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,8 +430,7 @@ DankPopout {
|
|||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
z: 10
|
z: 10
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (appList.hoverUpdatesSelection
|
if (appList.hoverUpdatesSelection && !appList.keyboardNavigationActive)
|
||||||
&& !appList.keyboardNavigationActive)
|
|
||||||
appList.currentIndex = index
|
appList.currentIndex = index
|
||||||
}
|
}
|
||||||
onPositionChanged: {
|
onPositionChanged: {
|
||||||
@@ -496,16 +438,10 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
onClicked: mouse => {
|
onClicked: mouse => {
|
||||||
if (mouse.button === Qt.LeftButton) {
|
if (mouse.button === Qt.LeftButton) {
|
||||||
appList.itemClicked(
|
appList.itemClicked(index, model)
|
||||||
index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
var panelPos = mapToItem(
|
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
|
||||||
contextMenu.parent,
|
appList.itemRightClicked(index, model, panelPos.x, panelPos.y)
|
||||||
mouse.x, mouse.y)
|
|
||||||
appList.itemRightClicked(
|
|
||||||
index, model,
|
|
||||||
panelPos.x,
|
|
||||||
panelPos.y)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,8 +475,7 @@ DankPopout {
|
|||||||
if (index < 0 || index >= count)
|
if (index < 0 || index >= count)
|
||||||
return
|
return
|
||||||
|
|
||||||
var itemY = Math.floor(
|
var itemY = Math.floor(index / actualColumns) * cellHeight
|
||||||
index / actualColumns) * cellHeight
|
|
||||||
var itemBottom = itemY + cellHeight
|
var itemBottom = itemY + cellHeight
|
||||||
if (itemY < contentY)
|
if (itemY < contentY)
|
||||||
contentY = itemY
|
contentY = itemY
|
||||||
@@ -555,8 +490,7 @@ DankPopout {
|
|||||||
clip: true
|
clip: true
|
||||||
cellWidth: baseCellWidth
|
cellWidth: baseCellWidth
|
||||||
cellHeight: baseCellHeight
|
cellHeight: baseCellHeight
|
||||||
leftMargin: Math.max(Theme.spacingS,
|
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||||
remainingSpace / 2)
|
|
||||||
rightMargin: leftMargin
|
rightMargin: leftMargin
|
||||||
focus: true
|
focus: true
|
||||||
interactive: true
|
interactive: true
|
||||||
@@ -578,14 +512,6 @@ DankPopout {
|
|||||||
appLauncher.keyboardNavigationActive = false
|
appLauncher.keyboardNavigationActive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: appGrid.cellWidth - appGrid.cellPadding
|
width: appGrid.cellWidth - appGrid.cellPadding
|
||||||
height: appGrid.cellHeight - appGrid.cellPadding
|
height: appGrid.cellHeight - appGrid.cellPadding
|
||||||
@@ -599,12 +525,7 @@ DankPopout {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
property int iconSize: Math.min(
|
property int iconSize: Math.min(appGrid.maxIconSize, Math.max(appGrid.minIconSize, appGrid.cellWidth * appGrid.iconSizeRatio))
|
||||||
appGrid.maxIconSize,
|
|
||||||
Math.max(
|
|
||||||
appGrid.minIconSize,
|
|
||||||
appGrid.cellWidth
|
|
||||||
* appGrid.iconSizeRatio))
|
|
||||||
|
|
||||||
width: iconSize
|
width: iconSize
|
||||||
height: iconSize
|
height: iconSize
|
||||||
@@ -614,9 +535,7 @@ DankPopout {
|
|||||||
id: gridIconImg
|
id: gridIconImg
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: (model.icon) ? Quickshell.iconPath(
|
source: Quickshell.iconPath(model.icon, true)
|
||||||
model.icon,
|
|
||||||
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: status === Image.Ready
|
visible: status === Image.Ready
|
||||||
@@ -632,14 +551,8 @@ DankPopout {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: (model.name
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
&& model.name.length
|
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||||
> 0) ? model.name.charAt(
|
|
||||||
0).toUpperCase(
|
|
||||||
) : "A"
|
|
||||||
font.pixelSize: Math.min(
|
|
||||||
28,
|
|
||||||
parent.width * 0.5)
|
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
@@ -669,8 +582,7 @@ DankPopout {
|
|||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
z: 10
|
z: 10
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (appGrid.hoverUpdatesSelection
|
if (appGrid.hoverUpdatesSelection && !appGrid.keyboardNavigationActive)
|
||||||
&& !appGrid.keyboardNavigationActive)
|
|
||||||
appGrid.currentIndex = index
|
appGrid.currentIndex = index
|
||||||
}
|
}
|
||||||
onPositionChanged: {
|
onPositionChanged: {
|
||||||
@@ -678,16 +590,10 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
onClicked: mouse => {
|
onClicked: mouse => {
|
||||||
if (mouse.button === Qt.LeftButton) {
|
if (mouse.button === Qt.LeftButton) {
|
||||||
appGrid.itemClicked(
|
appGrid.itemClicked(index, model)
|
||||||
index, model)
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
var panelPos = mapToItem(
|
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
|
||||||
contextMenu.parent,
|
appGrid.itemRightClicked(index, model, panelPos.x, panelPos.y)
|
||||||
mouse.x, mouse.y)
|
|
||||||
appGrid.itemRightClicked(
|
|
||||||
index, model,
|
|
||||||
panelPos.x,
|
|
||||||
panelPos.y)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -705,6 +611,9 @@ DankPopout {
|
|||||||
property var currentApp: null
|
property var currentApp: null
|
||||||
property bool menuVisible: false
|
property bool menuVisible: false
|
||||||
|
|
||||||
|
readonly property string appId: (currentApp && currentApp.desktopEntry) ? (currentApp.desktopEntry.id || currentApp.desktopEntry.execString || "") : ""
|
||||||
|
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
|
||||||
|
|
||||||
function show(x, y, app) {
|
function show(x, y, app) {
|
||||||
currentApp = app
|
currentApp = app
|
||||||
|
|
||||||
@@ -722,12 +631,8 @@ DankPopout {
|
|||||||
finalY = y - menuHeight - 8
|
finalY = y - menuHeight - 8
|
||||||
}
|
}
|
||||||
|
|
||||||
finalX = Math.max(
|
finalX = Math.max(8, Math.min(finalX, appDrawerPopout.popupWidth - menuWidth - 8))
|
||||||
8, Math.min(finalX,
|
finalY = Math.max(8, Math.min(finalY, appDrawerPopout.popupHeight - menuHeight - 8))
|
||||||
appDrawerPopout.popupWidth - menuWidth - 8))
|
|
||||||
finalY = Math.max(8, Math.min(
|
|
||||||
finalY,
|
|
||||||
appDrawerPopout.popupHeight - menuHeight - 8))
|
|
||||||
|
|
||||||
contextMenu.x = finalX
|
contextMenu.x = finalX
|
||||||
contextMenu.y = finalY
|
contextMenu.y = finalY
|
||||||
@@ -747,8 +652,7 @@ DankPopout {
|
|||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.popupBackground()
|
color: Theme.popupBackground()
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
border.width: 1
|
||||||
z: 1000
|
z: 1000
|
||||||
opacity: menuVisible ? 1 : 0
|
opacity: menuVisible ? 1 : 0
|
||||||
@@ -776,11 +680,7 @@ DankPopout {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 32
|
height: 32
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: pinMouseArea.containsMouse ? Qt.rgba(
|
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -789,17 +689,7 @@ DankPopout {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: {
|
name: contextMenu.isPinned ? "keep_off" : "push_pin"
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "push_pin"
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
return SessionData.isPinnedApp(
|
|
||||||
appId) ? "keep_off" : "push_pin"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
@@ -807,17 +697,7 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: contextMenu.isPinned ? "Unpin from Dock" : "Pin to Dock"
|
||||||
if (!contextMenu.currentApp
|
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return "Pin to Dock"
|
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
|
||||||
|| ""
|
|
||||||
return SessionData.isPinnedApp(
|
|
||||||
appId) ? "Unpin from Dock" : "Pin to Dock"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Normal
|
font.weight: Font.Normal
|
||||||
@@ -832,17 +712,15 @@ DankPopout {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!contextMenu.currentApp
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) {
|
||||||
|| !contextMenu.currentApp.desktopEntry)
|
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var appId = contextMenu.currentApp.desktopEntry.id
|
if (contextMenu.isPinned) {
|
||||||
|| contextMenu.currentApp.desktopEntry.execString
|
SessionData.removePinnedApp(contextMenu.appId)
|
||||||
|| ""
|
} else {
|
||||||
if (SessionData.isPinnedApp(appId))
|
SessionData.addPinnedApp(contextMenu.appId)
|
||||||
SessionData.removePinnedApp(appId)
|
}
|
||||||
else
|
|
||||||
SessionData.addPinnedApp(appId)
|
|
||||||
contextMenu.close()
|
contextMenu.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -858,8 +736,7 @@ DankPopout {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 1
|
height: 1
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,11 +744,7 @@ DankPopout {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 32
|
height: 32
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: launchMouseArea.containsMouse ? Qt.rgba(
|
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -17,20 +17,13 @@ Item {
|
|||||||
property bool debounceSearch: true
|
property bool debounceSearch: true
|
||||||
property int debounceInterval: 50
|
property int debounceInterval: 50
|
||||||
property bool keyboardNavigationActive: false
|
property bool keyboardNavigationActive: false
|
||||||
property var categories: {
|
property bool suppressUpdatesWhileLaunching: false
|
||||||
var allCategories = AppSearchService.getAllCategories().filter(cat => {
|
readonly property var categories: {
|
||||||
return cat !== "Education"
|
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||||
&& cat !== "Science"
|
const result = ["All"]
|
||||||
})
|
return result.concat(allCategories.filter(cat => cat !== "All"))
|
||||||
var result = ["All"]
|
|
||||||
return result.concat(allCategories.filter(cat => {
|
|
||||||
return cat !== "All"
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
property var categoryIcons: categories.map(category => {
|
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
|
||||||
return AppSearchService.getCategoryIcon(
|
|
||||||
category)
|
|
||||||
})
|
|
||||||
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
||||||
property alias model: filteredModel
|
property alias model: filteredModel
|
||||||
property var _watchApplications: AppSearchService.applications
|
property var _watchApplications: AppSearchService.applications
|
||||||
@@ -40,120 +33,105 @@ Item {
|
|||||||
signal viewModeSelected(string mode)
|
signal viewModeSelected(string mode)
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
|
if (suppressUpdatesWhileLaunching) {
|
||||||
|
suppressUpdatesWhileLaunching = false
|
||||||
|
return
|
||||||
|
}
|
||||||
filteredModel.clear()
|
filteredModel.clear()
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
keyboardNavigationActive = false
|
keyboardNavigationActive = false
|
||||||
var apps = []
|
|
||||||
|
let apps = []
|
||||||
if (searchQuery.length === 0) {
|
if (searchQuery.length === 0) {
|
||||||
if (selectedCategory === "All") {
|
apps = selectedCategory === "All" ? AppSearchService.getAppsInCategory("All") : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
||||||
apps = AppSearchService.getAppsInCategory(
|
|
||||||
"All") // HACK: Use function call instead of property
|
|
||||||
} else {
|
|
||||||
var categoryApps = AppSearchService.getAppsInCategory(
|
|
||||||
selectedCategory)
|
|
||||||
apps = categoryApps.slice(0, maxResults)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (selectedCategory === "All") {
|
if (selectedCategory === "All") {
|
||||||
apps = AppSearchService.searchApplications(searchQuery)
|
apps = AppSearchService.searchApplications(searchQuery)
|
||||||
} else {
|
} else {
|
||||||
var categoryApps = AppSearchService.getAppsInCategory(
|
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
||||||
selectedCategory)
|
|
||||||
if (categoryApps.length > 0) {
|
if (categoryApps.length > 0) {
|
||||||
var allSearchResults = AppSearchService.searchApplications(
|
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
||||||
searchQuery)
|
const categoryNames = new Set(categoryApps.map(app => app.name))
|
||||||
var categoryNames = new Set(categoryApps.map(app => {
|
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
||||||
return app.name
|
|
||||||
}))
|
|
||||||
apps = allSearchResults.filter(searchApp => {
|
|
||||||
return categoryNames.has(
|
|
||||||
searchApp.name)
|
|
||||||
}).slice(0, maxResults)
|
|
||||||
} else {
|
} else {
|
||||||
apps = []
|
apps = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (searchQuery.length === 0)
|
|
||||||
apps = apps.sort(function (a, b) {
|
|
||||||
var aId = a.id || (a.execString || a.exec || "")
|
|
||||||
var bId = b.id || (b.execString || b.exec || "")
|
|
||||||
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
|
||||||
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
|
||||||
if (aUsage !== bUsage)
|
|
||||||
return bUsage - aUsage
|
|
||||||
|
|
||||||
return (a.name || "").localeCompare(b.name || "")
|
if (searchQuery.length === 0) {
|
||||||
})
|
apps = apps.sort((a, b) => {
|
||||||
|
const aId = a.id || a.execString || a.exec || ""
|
||||||
|
const bId = b.id || b.execString || b.exec || ""
|
||||||
|
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
||||||
|
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
||||||
|
if (aUsage !== bUsage) {
|
||||||
|
return bUsage - aUsage
|
||||||
|
}
|
||||||
|
return (a.name || "").localeCompare(b.name || "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
apps.forEach(app => {
|
apps.forEach(app => {
|
||||||
if (app)
|
if (app) {
|
||||||
filteredModel.append({
|
filteredModel.append({
|
||||||
"name": app.name || "",
|
"name": app.name || "",
|
||||||
"exec": app.execString || "",
|
"exec": app.execString || "",
|
||||||
"icon": app.icon
|
"icon": app.icon || "application-x-executable",
|
||||||
|| "application-x-executable",
|
"comment": app.comment || "",
|
||||||
"comment": app.comment || "",
|
"categories": app.categories || [],
|
||||||
"categories": app.categories
|
"desktopEntry": app
|
||||||
|| [],
|
})
|
||||||
"desktopEntry": app
|
}
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNext() {
|
function selectNext() {
|
||||||
if (filteredModel.count > 0) {
|
if (filteredModel.count === 0) {
|
||||||
keyboardNavigationActive = true
|
return
|
||||||
if (viewMode === "grid") {
|
|
||||||
var newIndex = Math.min(selectedIndex + gridColumns,
|
|
||||||
filteredModel.count - 1)
|
|
||||||
selectedIndex = newIndex
|
|
||||||
} else {
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1,
|
|
||||||
filteredModel.count - 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPrevious() {
|
function selectPrevious() {
|
||||||
if (filteredModel.count > 0) {
|
if (filteredModel.count === 0) {
|
||||||
keyboardNavigationActive = true
|
return
|
||||||
if (viewMode === "grid") {
|
|
||||||
var newIndex = Math.max(selectedIndex - gridColumns, 0)
|
|
||||||
selectedIndex = newIndex
|
|
||||||
} else {
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNextInRow() {
|
function selectNextInRow() {
|
||||||
if (filteredModel.count > 0 && viewMode === "grid") {
|
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||||
keyboardNavigationActive = true
|
return
|
||||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
|
||||||
}
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPreviousInRow() {
|
function selectPreviousInRow() {
|
||||||
if (filteredModel.count > 0 && viewMode === "grid") {
|
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||||
keyboardNavigationActive = true
|
return
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchSelected() {
|
function launchSelected() {
|
||||||
if (filteredModel.count > 0 && selectedIndex >= 0
|
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
|
||||||
&& selectedIndex < filteredModel.count) {
|
return
|
||||||
var selectedApp = filteredModel.get(selectedIndex)
|
|
||||||
launchApp(selectedApp)
|
|
||||||
}
|
}
|
||||||
|
const selectedApp = filteredModel.get(selectedIndex)
|
||||||
|
launchApp(selectedApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchApp(appData) {
|
function launchApp(appData) {
|
||||||
if (!appData)
|
if (!appData) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
appData.desktopEntry.execute()
|
suppressUpdatesWhileLaunching = true
|
||||||
|
SessionService.launchDesktopEntry(appData.desktopEntry)
|
||||||
appLaunched(appData)
|
appLaunched(appData)
|
||||||
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
||||||
}
|
}
|
||||||
@@ -169,10 +147,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSearchQueryChanged: {
|
onSearchQueryChanged: {
|
||||||
if (debounceSearch)
|
if (debounceSearch) {
|
||||||
searchDebounceTimer.restart()
|
searchDebounceTimer.restart()
|
||||||
else
|
} else {
|
||||||
updateFilteredModel()
|
updateFilteredModel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onSelectedCategoryChanged: updateFilteredModel()
|
onSelectedCategoryChanged: updateFilteredModel()
|
||||||
onAppUsageRankingChanged: updateFilteredModel()
|
onAppUsageRankingChanged: updateFilteredModel()
|
||||||
|
|||||||
@@ -8,11 +8,25 @@ Item {
|
|||||||
|
|
||||||
property var categories: []
|
property var categories: []
|
||||||
property string selectedCategory: "All"
|
property string selectedCategory: "All"
|
||||||
property bool compact: false // For different layout styles
|
property bool compact: false
|
||||||
|
|
||||||
signal categorySelected(string category)
|
signal categorySelected(string category)
|
||||||
|
|
||||||
height: compact ? 36 : (72 + Theme.spacingS) // Single row vs two rows
|
readonly property int maxCompactItems: 8
|
||||||
|
readonly property int itemHeight: 36
|
||||||
|
readonly property color selectedBorderColor: "transparent"
|
||||||
|
readonly property color unselectedBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
|
|
||||||
|
function handleCategoryClick(category) {
|
||||||
|
selectedCategory = category
|
||||||
|
categorySelected(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getButtonWidth(itemCount, containerWidth) {
|
||||||
|
return itemCount > 0 ? (containerWidth - (itemCount - 1) * Theme.spacingS) / itemCount : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
height: compact ? itemHeight : (itemHeight * 2 + Theme.spacingS)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
visible: compact
|
visible: compact
|
||||||
@@ -20,22 +34,16 @@ Item {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: categories.slice(0, Math.min(categories.length,
|
model: categories ? categories.slice(0, Math.min(categories.length || 0, maxCompactItems)) : []
|
||||||
8)) // Limit for space
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
height: 36
|
property int itemCount: Math.min(categories ? categories.length || 0 : 0, maxCompactItems)
|
||||||
width: (parent.width - (Math.min(
|
|
||||||
categories.length,
|
height: root.itemHeight
|
||||||
8) - 1) * Theme.spacingS) / Math.min(
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
categories.length, 8)
|
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||||
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(
|
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b,
|
|
||||||
0.3)
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -50,10 +58,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: root.handleCategoryClick(modelData)
|
||||||
selectedCategory = modelData
|
|
||||||
categorySelected(modelData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,27 +70,20 @@ Item {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
property var firstRowCategories: categories.slice(
|
|
||||||
0, Math.min(4,
|
|
||||||
categories.length))
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: parent.firstRowCategories
|
model: categories ? categories.slice(0, Math.min(4, categories.length || 0)) : []
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
height: 36
|
property int itemCount: Math.min(4, categories ? categories.length || 0 : 0)
|
||||||
width: (parent.width - (parent.firstRowCategories.length - 1)
|
|
||||||
* Theme.spacingS) / parent.firstRowCategories.length
|
height: root.itemHeight
|
||||||
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||||
border.color: selectedCategory
|
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||||
=== modelData ? "transparent" : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -100,37 +98,28 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: root.handleCategoryClick(modelData)
|
||||||
selectedCategory = modelData
|
|
||||||
categorySelected(modelData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
property var secondRowCategories: categories.slice(
|
|
||||||
4, categories.length)
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
visible: secondRowCategories.length > 0
|
visible: categories && categories.length > 4
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: parent.secondRowCategories
|
model: categories && categories.length > 4 ? categories.slice(4) : []
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
height: 36
|
property int itemCount: categories && categories.length > 4 ? categories.length - 4 : 0
|
||||||
width: (parent.width - (parent.secondRowCategories.length - 1)
|
|
||||||
* Theme.spacingS) / parent.secondRowCategories.length
|
height: root.itemHeight
|
||||||
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||||
border.color: selectedCategory
|
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||||
=== modelData ? "transparent" : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.3)
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -145,10 +134,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: root.handleCategoryClick(modelData)
|
||||||
selectedCategory = modelData
|
|
||||||
categorySelected(modelData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: calendarGrid
|
|
||||||
|
|
||||||
property date displayDate: new Date()
|
|
||||||
property date selectedDate: new Date()
|
|
||||||
|
|
||||||
function loadEventsForMonth() {
|
|
||||||
if (!CalendarService || !CalendarService.khalAvailable)
|
|
||||||
return
|
|
||||||
|
|
||||||
let firstDay = new Date(displayDate.getFullYear(),
|
|
||||||
displayDate.getMonth(), 1)
|
|
||||||
let dayOfWeek = firstDay.getDay()
|
|
||||||
let startDate = new Date(firstDay)
|
|
||||||
startDate.setDate(startDate.getDate(
|
|
||||||
) - dayOfWeek - 7) // Extra week padding
|
|
||||||
let lastDay = new Date(displayDate.getFullYear(),
|
|
||||||
displayDate.getMonth() + 1, 0)
|
|
||||||
let endDate = new Date(lastDay)
|
|
||||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay(
|
|
||||||
)) + 7) // Extra week padding
|
|
||||||
CalendarService.loadEvents(startDate, endDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
onDisplayDateChanged: {
|
|
||||||
loadEventsForMonth()
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
loadEventsForMonth()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onKhalAvailableChanged() {
|
|
||||||
if (CalendarService && CalendarService.khalAvailable)
|
|
||||||
loadEventsForMonth()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: CalendarService
|
|
||||||
enabled: CalendarService !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "chevron_left"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: prevMonthArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
let newDate = new Date(displayDate)
|
|
||||||
newDate.setMonth(newDate.getMonth() - 1)
|
|
||||||
displayDate = newDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width - 80
|
|
||||||
height: 40
|
|
||||||
text: Qt.formatDate(displayDate, "MMMM yyyy")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "chevron_right"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nextMonthArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
let newDate = new Date(displayDate)
|
|
||||||
newDate.setMonth(newDate.getMonth() + 1)
|
|
||||||
displayDate = newDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width / 7
|
|
||||||
height: 32
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid {
|
|
||||||
property date firstDay: {
|
|
||||||
let date = new Date(displayDate.getFullYear(),
|
|
||||||
displayDate.getMonth(), 1)
|
|
||||||
let dayOfWeek = date.getDay()
|
|
||||||
date.setDate(date.getDate() - dayOfWeek)
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 200 // Fixed height for calendar
|
|
||||||
columns: 7
|
|
||||||
rows: 6
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: 42
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property date dayDate: {
|
|
||||||
let date = new Date(parent.firstDay)
|
|
||||||
date.setDate(date.getDate() + index)
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
property bool isCurrentMonth: dayDate.getMonth(
|
|
||||||
) === displayDate.getMonth()
|
|
||||||
property bool isToday: dayDate.toDateString(
|
|
||||||
) === new Date().toDateString()
|
|
||||||
property bool isSelected: dayDate.toDateString(
|
|
||||||
) === selectedDate.toDateString()
|
|
||||||
|
|
||||||
width: parent.width / 7
|
|
||||||
height: parent.height / 6
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - 4
|
|
||||||
height: parent.height - 4
|
|
||||||
color: isSelected ? Theme.primary : isToday ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: dayDate.getDate()
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
|
||||||
font.weight: isToday
|
|
||||||
|| isSelected ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: eventIndicator
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: parent.radius
|
|
||||||
visible: CalendarService
|
|
||||||
&& CalendarService.khalAvailable
|
|
||||||
&& CalendarService.hasEventsForDate(dayDate)
|
|
||||||
opacity: {
|
|
||||||
if (isSelected)
|
|
||||||
return 0.9
|
|
||||||
else if (isToday)
|
|
||||||
return 0.8
|
|
||||||
else
|
|
||||||
return 0.6
|
|
||||||
}
|
|
||||||
|
|
||||||
gradient: Gradient {
|
|
||||||
GradientStop {
|
|
||||||
position: 0.89
|
|
||||||
color: "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
position: 0.9
|
|
||||||
color: {
|
|
||||||
if (isSelected)
|
|
||||||
return Qt.lighter(Theme.primary, 1.3)
|
|
||||||
else if (isToday)
|
|
||||||
return Theme.primary
|
|
||||||
else
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
position: 1
|
|
||||||
color: {
|
|
||||||
if (isSelected)
|
|
||||||
return Qt.lighter(Theme.primary, 1.3)
|
|
||||||
else if (isToday)
|
|
||||||
return Theme.primary
|
|
||||||
else
|
|
||||||
return Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dayArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
selectedDate = dayDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.CentcomCenter
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property bool hasActiveMedia: MprisController.activePlayer !== null
|
|
||||||
property bool calendarVisible: false
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property real triggerX: (Screen.width - 480) / 2
|
|
||||||
property real triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4
|
|
||||||
property real triggerWidth: 80
|
|
||||||
property string triggerSection: "center"
|
|
||||||
property var triggerScreen: null
|
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
|
||||||
triggerX = x
|
|
||||||
triggerY = y
|
|
||||||
triggerWidth = width
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: calendarVisible || closeTimer.running
|
|
||||||
screen: triggerScreen
|
|
||||||
onCalendarVisibleChanged: {
|
|
||||||
if (calendarVisible) {
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
Qt.callLater(() => {
|
|
||||||
calendarGrid.loadEventsForMonth()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
shouldBeVisible = false
|
|
||||||
closeTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
interval: Theme.mediumDuration + 50
|
|
||||||
onTriggered: {
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && calendarGrid)
|
|
||||||
calendarGrid.loadEventsForMonth()
|
|
||||||
}
|
|
||||||
implicitWidth: 480
|
|
||||||
implicitHeight: 600
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: mainContainer
|
|
||||||
|
|
||||||
readonly property real targetWidth: Math.min(
|
|
||||||
(root.screen ? root.screen.width : Screen.width)
|
|
||||||
* 0.9, 600)
|
|
||||||
|
|
||||||
function calculateWidth() {
|
|
||||||
let baseWidth = 320
|
|
||||||
if (leftWidgets.hasAnyWidgets)
|
|
||||||
return Math.min(parent.width * 0.9, 600)
|
|
||||||
|
|
||||||
return Math.min(parent.width * 0.7, 400)
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateHeight() {
|
|
||||||
let contentHeight = Theme.spacingM * 2
|
|
||||||
// margins
|
|
||||||
let widgetHeight = 160
|
|
||||||
widgetHeight += 140 + Theme.spacingM
|
|
||||||
let calendarHeight = 300
|
|
||||||
let mainRowHeight = Math.max(widgetHeight, calendarHeight)
|
|
||||||
contentHeight += mainRowHeight + Theme.spacingM
|
|
||||||
if (CalendarService && CalendarService.khalAvailable) {
|
|
||||||
let hasEvents = events.selectedDateEvents
|
|
||||||
&& events.selectedDateEvents.length > 0
|
|
||||||
let eventsHeight = hasEvents ? Math.min(
|
|
||||||
300,
|
|
||||||
80 + events.selectedDateEvents.length * 60) : 120
|
|
||||||
contentHeight += eventsHeight
|
|
||||||
} else {
|
|
||||||
contentHeight -= Theme.spacingM
|
|
||||||
}
|
|
||||||
return Math.min(contentHeight, parent.height * 0.9)
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real calculatedX: {
|
|
||||||
var screenWidth = root.screen ? root.screen.width : Screen.width
|
|
||||||
if (root.triggerSection === "center") {
|
|
||||||
return (screenWidth - targetWidth) / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
var centerX = root.triggerX + (root.triggerWidth / 2) - (targetWidth / 2)
|
|
||||||
|
|
||||||
if (centerX >= Theme.spacingM
|
|
||||||
&& centerX + targetWidth <= screenWidth - Theme.spacingM) {
|
|
||||||
return centerX
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centerX < Theme.spacingM) {
|
|
||||||
return Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
|
|
||||||
return screenWidth - targetWidth - Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
return centerX
|
|
||||||
}
|
|
||||||
|
|
||||||
width: targetWidth
|
|
||||||
height: calculateHeight()
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
layer.enabled: true
|
|
||||||
opacity: shouldBeVisible ? 1 : 0
|
|
||||||
scale: shouldBeVisible ? 1 : 0.9
|
|
||||||
x: calculatedX
|
|
||||||
y: root.triggerY
|
|
||||||
onOpacityChanged: {
|
|
||||||
if (opacity === 1)
|
|
||||||
Qt.callLater(() => {
|
|
||||||
height = calculateHeight()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onEventsByDateChanged() {
|
|
||||||
if (mainContainer.opacity === 1)
|
|
||||||
mainContainer.height = mainContainer.calculateHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKhalAvailableChanged() {
|
|
||||||
if (mainContainer.opacity === 1)
|
|
||||||
mainContainer.height = mainContainer.calculateHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: CalendarService
|
|
||||||
enabled: CalendarService !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onSelectedDateEventsChanged() {
|
|
||||||
if (mainContainer.opacity === 1)
|
|
||||||
mainContainer.height = mainContainer.calculateHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: events
|
|
||||||
enabled: events !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
|
|
||||||
Theme.surfaceTint.b, 0.04)
|
|
||||||
radius: parent.radius
|
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
|
||||||
running: shouldBeVisible
|
|
||||||
loops: Animation.Infinite
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
to: 0.08
|
|
||||||
duration: Theme.extraLongDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
to: 0.02
|
|
||||||
duration: Theme.extraLongDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
calendarVisible = false
|
|
||||||
event.accepted = true
|
|
||||||
} else {
|
|
||||||
// Don't handle other keys - let them bubble up to modals
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: {
|
|
||||||
let widgetHeight = 160
|
|
||||||
// Media widget
|
|
||||||
widgetHeight += 140 + Theme.spacingM // Weather/SystemInfo widget with spacing
|
|
||||||
let calendarHeight = 300
|
|
||||||
// Calendar
|
|
||||||
return Math.max(widgetHeight, calendarHeight)
|
|
||||||
}
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: leftWidgets
|
|
||||||
|
|
||||||
property bool hasAnyWidgets: true
|
|
||||||
|
|
||||||
width: hasAnyWidgets ? parent.width
|
|
||||||
* 0.42 : 0 // Slightly narrower for better proportions
|
|
||||||
height: childrenRect.height
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: hasAnyWidgets
|
|
||||||
anchors.top: parent.top
|
|
||||||
|
|
||||||
MediaPlayer {
|
|
||||||
width: parent.width
|
|
||||||
height: 160
|
|
||||||
}
|
|
||||||
|
|
||||||
Weather {
|
|
||||||
width: parent.width
|
|
||||||
height: 140
|
|
||||||
visible: SettingsData.weatherEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemInfo {
|
|
||||||
width: parent.width
|
|
||||||
height: 140
|
|
||||||
visible: !SettingsData.weatherEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width
|
|
||||||
- Theme.spacingM : parent.width
|
|
||||||
height: parent.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.2)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
CalendarGrid {
|
|
||||||
id: calendarGrid
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Events {
|
|
||||||
id: events
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
selectedDate: calendarGrid.selectedDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durMed
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durMed
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 4
|
|
||||||
shadowBlur: 0.5
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
shadowOpacity: 0.15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
z: -1
|
|
||||||
enabled: shouldBeVisible
|
|
||||||
onClicked: function (mouse) {
|
|
||||||
var localPos = mapToItem(mainContainer, mouse.x, mouse.y)
|
|
||||||
if (localPos.x < 0 || localPos.x > mainContainer.width
|
|
||||||
|| localPos.y < 0 || localPos.y > mainContainer.height)
|
|
||||||
calendarVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: events
|
|
||||||
|
|
||||||
property date selectedDate: new Date()
|
|
||||||
property var selectedDateEvents: []
|
|
||||||
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
|
|
||||||
property bool shouldShow: CalendarService && CalendarService.khalAvailable
|
|
||||||
|
|
||||||
function updateSelectedDateEvents() {
|
|
||||||
if (CalendarService && CalendarService.khalAvailable) {
|
|
||||||
let events = CalendarService.getEventsForDate(selectedDate)
|
|
||||||
selectedDateEvents = events
|
|
||||||
} else {
|
|
||||||
selectedDateEvents = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelectedDateEventsChanged: {
|
|
||||||
eventsList.model = selectedDateEvents
|
|
||||||
}
|
|
||||||
width: parent.width
|
|
||||||
height: shouldShow ? (hasEvents ? Math.min(
|
|
||||||
300,
|
|
||||||
80 + selectedDateEvents.length * 60) : 120) : 0
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.12)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
visible: shouldShow
|
|
||||||
layer.enabled: true
|
|
||||||
Component.onCompleted: {
|
|
||||||
updateSelectedDateEvents()
|
|
||||||
}
|
|
||||||
onSelectedDateChanged: {
|
|
||||||
updateSelectedDateEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onEventsByDateChanged() {
|
|
||||||
updateSelectedDateEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKhalAvailableChanged() {
|
|
||||||
updateSelectedDateEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
target: CalendarService
|
|
||||||
enabled: CalendarService !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: headerRow
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "event"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • "
|
|
||||||
+ (selectedDateEvents.length
|
|
||||||
=== 1 ? "1 event" : selectedDateEvents.length
|
|
||||||
+ " events")) : Qt.formatDate(
|
|
||||||
selectedDate, "MMM d")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: !hasEvents
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "event_busy"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.3)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No events"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: eventsList
|
|
||||||
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
visible: opacity > 0
|
|
||||||
opacity: hasEvents ? 1 : 0
|
|
||||||
clip: true
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
interactive: true
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
|
||||||
pressDelay: 0
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
property real momentum: 0
|
|
||||||
onWheel: event => {
|
|
||||||
if (event.pixelDelta.y !== 0) {
|
|
||||||
// Touchpad with pixel delta
|
|
||||||
momentum = event.pixelDelta.y * 1.8
|
|
||||||
} else {
|
|
||||||
// Mouse wheel with angle delta
|
|
||||||
momentum = (event.angleDelta.y / 120)
|
|
||||||
* (60 * 2.5) // ~2.5 items per wheel step
|
|
||||||
}
|
|
||||||
|
|
||||||
let newY = parent.contentY - momentum
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
momentum *= 0.92 // Decay for smooth momentum
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: eventsList.contentHeight
|
|
||||||
> eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: eventsList.width
|
|
||||||
height: eventContent.implicitHeight + Theme.spacingM
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (modelData.url && eventMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
else if (eventMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.06)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.06)
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
if (modelData.url && eventMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.3)
|
|
||||||
else if (eventMouseArea.containsMouse)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.15)
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 4
|
|
||||||
height: parent.height - 8
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 4
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
radius: 2
|
|
||||||
color: Theme.primary
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: eventContent
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingL + 4
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: modelData.title
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
maximumLineCount: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Math.max(timeRow.height, locationRow.height)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: timeRow
|
|
||||||
|
|
||||||
spacing: 4
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "schedule"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (modelData.allDay) {
|
|
||||||
return "All day"
|
|
||||||
} else {
|
|
||||||
let timeFormat = SettingsData.use24HourClock ? "H:mm" : "h:mm AP"
|
|
||||||
let startTime = Qt.formatTime(
|
|
||||||
modelData.start, timeFormat)
|
|
||||||
if (modelData.start.toDateString(
|
|
||||||
) !== modelData.end.toDateString(
|
|
||||||
) || modelData.start.getTime(
|
|
||||||
) !== modelData.end.getTime())
|
|
||||||
return startTime + " – " + Qt.formatTime(
|
|
||||||
modelData.end, timeFormat)
|
|
||||||
|
|
||||||
return startTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: locationRow
|
|
||||||
|
|
||||||
spacing: 4
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: modelData.location !== ""
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "location_on"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.location
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
maximumLineCount: 1
|
|
||||||
width: Math.min(implicitWidth, 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: eventMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: modelData.url !== ""
|
|
||||||
onClicked: {
|
|
||||||
if (modelData.url && modelData.url !== "") {
|
|
||||||
if (Qt.openUrlExternally(modelData.url) === false)
|
|
||||||
console.warn("Failed to open URL: " + modelData.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.25
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
|
||||||
shadowOpacity: 0.1
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,471 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: mediaPlayer
|
|
||||||
|
|
||||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
|
||||||
property string lastValidTitle: ""
|
|
||||||
property string lastValidArtist: ""
|
|
||||||
property string lastValidAlbum: ""
|
|
||||||
property string lastValidArtUrl: ""
|
|
||||||
property real currentPosition: activePlayer
|
|
||||||
&& activePlayer.positionSupported ? activePlayer.position : 0
|
|
||||||
|
|
||||||
function ratio() {
|
|
||||||
if (!activePlayer || activePlayer.length <= 0) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
let calculatedRatio = currentPosition / activePlayer.length
|
|
||||||
return Math.max(0, Math.min(1, calculatedRatio))
|
|
||||||
}
|
|
||||||
|
|
||||||
onActivePlayerChanged: {
|
|
||||||
if (activePlayer && activePlayer.positionSupported) {
|
|
||||||
activePlayer.positionChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: positionTimer
|
|
||||||
|
|
||||||
interval: 500
|
|
||||||
running: activePlayer
|
|
||||||
&& activePlayer.playbackState === MprisPlaybackState.Playing
|
|
||||||
&& !progressMouseArea.isSeeking
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
if (activePlayer && activePlayer.positionSupported) {
|
|
||||||
activePlayer.positionChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: cleanupTimer
|
|
||||||
|
|
||||||
interval: 2000
|
|
||||||
running: !activePlayer
|
|
||||||
onTriggered: {
|
|
||||||
lastValidTitle = ""
|
|
||||||
lastValidArtist = ""
|
|
||||||
lastValidAlbum = ""
|
|
||||||
lastValidArtUrl = ""
|
|
||||||
currentPosition = 0
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onTrackChanged() {
|
|
||||||
if (activePlayer && activePlayer.positionSupported) {
|
|
||||||
activePlayer.positionChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: activePlayer
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: (!activePlayer && !lastValidTitle)
|
|
||||||
|| (activePlayer && activePlayer.trackTitle === ""
|
|
||||||
&& lastValidTitle === "")
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "music_note"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No Media Playing"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: activePlayer && activePlayer.trackTitle !== ""
|
|
||||||
|| lastValidTitle !== ""
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 60
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: albumArt
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: activePlayer && activePlayer.trackArtUrl
|
|
||||||
|| lastValidArtUrl || ""
|
|
||||||
onSourceChanged: {
|
|
||||||
if (activePlayer && activePlayer.trackArtUrl)
|
|
||||||
lastValidArtUrl = activePlayer.trackArtUrl
|
|
||||||
}
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
smooth: true
|
|
||||||
cache: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: albumArt.status !== Image.Ready
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "album"
|
|
||||||
size: 28
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 60 - Theme.spacingM
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: activePlayer && activePlayer.trackTitle
|
|
||||||
|| lastValidTitle || "Unknown Track"
|
|
||||||
onTextChanged: {
|
|
||||||
if (activePlayer && activePlayer.trackTitle)
|
|
||||||
lastValidTitle = activePlayer.trackTitle
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: activePlayer && activePlayer.trackArtist
|
|
||||||
|| lastValidArtist || "Unknown Artist"
|
|
||||||
onTextChanged: {
|
|
||||||
if (activePlayer && activePlayer.trackArtist)
|
|
||||||
lastValidArtist = activePlayer.trackArtist
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.8)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: activePlayer && activePlayer.trackAlbum
|
|
||||||
|| lastValidAlbum || ""
|
|
||||||
onTextChanged: {
|
|
||||||
if (activePlayer && activePlayer.trackAlbum)
|
|
||||||
lastValidAlbum = activePlayer.trackAlbum
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: progressBarContainer
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: progressBarBackground
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 6
|
|
||||||
radius: 3
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
visible: activePlayer !== null
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: progressFill
|
|
||||||
|
|
||||||
height: parent.height
|
|
||||||
radius: parent.radius
|
|
||||||
color: Theme.primary
|
|
||||||
width: Math.max(0, Math.min(parent.width,
|
|
||||||
parent.width * ratio()))
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: progressHandle
|
|
||||||
|
|
||||||
width: 12
|
|
||||||
height: 12
|
|
||||||
radius: 6
|
|
||||||
color: Theme.primary
|
|
||||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
|
||||||
border.width: 1
|
|
||||||
x: Math.max(0, Math.min(parent.width - width,
|
|
||||||
progressFill.width - width / 2))
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: activePlayer && activePlayer.length > 0
|
|
||||||
scale: progressMouseArea.containsMouse
|
|
||||||
|| progressMouseArea.pressed ? 1.2 : 1
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 150
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: progressMouseArea
|
|
||||||
|
|
||||||
property bool isSeeking: false
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: activePlayer && activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek
|
|
||||||
preventStealing: true
|
|
||||||
onPressed: function (mouse) {
|
|
||||||
isSeeking = true
|
|
||||||
if (activePlayer && activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
mouse.x / progressBarBackground.width))
|
|
||||||
let seekPosition = ratio * activePlayer.length
|
|
||||||
activePlayer.position = seekPosition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
isSeeking = false
|
|
||||||
}
|
|
||||||
onPositionChanged: function (mouse) {
|
|
||||||
if (pressed && isSeeking && activePlayer
|
|
||||||
&& activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
mouse.x / progressBarBackground.width))
|
|
||||||
let seekPosition = ratio * activePlayer.length
|
|
||||||
activePlayer.position = seekPosition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClicked: function (mouse) {
|
|
||||||
if (activePlayer && activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
mouse.x / progressBarBackground.width))
|
|
||||||
let seekPosition = ratio * activePlayer.length
|
|
||||||
activePlayer.position = seekPosition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: progressGlobalMouseArea
|
|
||||||
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: mediaPlayer.width
|
|
||||||
height: mediaPlayer.height
|
|
||||||
enabled: progressMouseArea.isSeeking
|
|
||||||
visible: false
|
|
||||||
preventStealing: true
|
|
||||||
onPositionChanged: function (mouse) {
|
|
||||||
if (progressMouseArea.isSeeking && activePlayer
|
|
||||||
&& activePlayer.length > 0
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
let globalPos = mapToItem(progressBarBackground,
|
|
||||||
mouse.x, mouse.y)
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
globalPos.x / progressBarBackground.width))
|
|
||||||
let seekPosition = ratio * activePlayer.length
|
|
||||||
activePlayer.position = seekPosition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
progressMouseArea.isSeeking = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
visible: activePlayer !== null
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
height: parent.height
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: prevBtnArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_previous"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: prevBtnArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!activePlayer)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (activePlayer.position > 8
|
|
||||||
&& activePlayer.canSeek) {
|
|
||||||
activePlayer.position = 0
|
|
||||||
} else {
|
|
||||||
activePlayer.previous()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Theme.primary
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: activePlayer && activePlayer.playbackState
|
|
||||||
=== MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
|
||||||
size: 20
|
|
||||||
color: Theme.background
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer
|
|
||||||
&& activePlayer.togglePlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: nextBtnArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_next"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nextBtnArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer && activePlayer.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.5
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
|
||||||
shadowOpacity: 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: DgopService
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
SystemLogo {
|
|
||||||
width: 48
|
|
||||||
height: 48
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 48 - Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.hostname
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.distribution + " • " + DgopService.architecture
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Uptime " + formatUptime(UserInfoService.uptime)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Load: " + DgopService.loadAverage
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: DgopService.processCount + " proc, " + DgopService.threadCount + " threads"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.8)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUptime(uptime) {
|
|
||||||
if (!uptime)
|
|
||||||
return "0m"
|
|
||||||
|
|
||||||
// Parse the uptime string - handle formats like "1 week, 4 days, 3:45" or "4 days, 3:45" or "3:45"
|
|
||||||
var uptimeStr = uptime.toString().trim()
|
|
||||||
|
|
||||||
// Check for weeks and days - need to add them together
|
|
||||||
var weekMatch = uptimeStr.match(/(\d+)\s+weeks?/)
|
|
||||||
var dayMatch = uptimeStr.match(/(\d+)\s+days?/)
|
|
||||||
|
|
||||||
if (weekMatch) {
|
|
||||||
var weeks = parseInt(weekMatch[1])
|
|
||||||
var totalDays = weeks * 7
|
|
||||||
if (dayMatch) {
|
|
||||||
var days = parseInt(dayMatch[1])
|
|
||||||
totalDays += days
|
|
||||||
}
|
|
||||||
return totalDays + "d"
|
|
||||||
} else if (dayMatch) {
|
|
||||||
var days = parseInt(dayMatch[1])
|
|
||||||
return days + "d"
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's just hours:minutes, show the largest unit
|
|
||||||
var timeMatch = uptimeStr.match(/(\d+):(\d+)/)
|
|
||||||
if (timeMatch) {
|
|
||||||
var hours = parseInt(timeMatch[1])
|
|
||||||
var minutes = parseInt(timeMatch[2])
|
|
||||||
if (hours > 0) {
|
|
||||||
return hours + "h"
|
|
||||||
} else {
|
|
||||||
return minutes + "m"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback - return as is but truncated
|
|
||||||
return uptimeStr.length > 8 ? uptimeStr.substring(0,
|
|
||||||
8) + "…" : uptimeStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: weather
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: WeatherService
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: !WeatherService.weather.available
|
|
||||||
|| WeatherService.weather.temp === 0
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "cloud_off"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No Weather Data"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: WeatherService.weather.available
|
|
||||||
&& WeatherService.weather.temp !== 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: refreshButton
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.3)
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.rightMargin: -Theme.spacingS
|
|
||||||
anchors.topMargin: -Theme.spacingS
|
|
||||||
|
|
||||||
property bool isRefreshing: false
|
|
||||||
enabled: !isRefreshing
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
|
||||||
onClicked: {
|
|
||||||
refreshButton.isRefreshing = true
|
|
||||||
WeatherService.forceRefresh()
|
|
||||||
refreshTimer.restart()
|
|
||||||
}
|
|
||||||
enabled: parent.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: refreshTimer
|
|
||||||
interval: 2000
|
|
||||||
onTriggered: refreshButton.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation on rotation {
|
|
||||||
running: refreshButton.isRefreshing
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(
|
|
||||||
WeatherService.weather.wCode)
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp)
|
|
||||||
+ "°" + (SettingsData.useFahrenheit ? "F" : "C")
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Light
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (WeatherService.weather.available)
|
|
||||||
SettingsData.setTemperatureUnit(
|
|
||||||
!SettingsData.useFahrenheit)
|
|
||||||
}
|
|
||||||
enabled: WeatherService.weather.available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.city || ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid {
|
|
||||||
columns: 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "humidity_low"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.humidity ? WeatherService.weather.humidity
|
|
||||||
+ "%" : "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "air"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.wind || "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "wb_twilight"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.sunrise || "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.weather.sunset || "--"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.5
|
|
||||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
|
||||||
shadowOpacity: 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(
|
|
||||||
AudioService.sink) : ""
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Output Device"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 35
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
visible: AudioService.sink !== null
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "check_circle"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Current: " + (root.currentSinkDisplayName || "None")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
|
|
||||||
return []
|
|
||||||
|
|
||||||
let sinks = []
|
|
||||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
|
||||||
let node = Pipewire.nodes.values[i]
|
|
||||||
if (!node || node.isStream)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink)
|
|
||||||
sinks.push(node)
|
|
||||||
}
|
|
||||||
return sinks
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: deviceArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (modelData.name.includes("bluez"))
|
|
||||||
return "headset"
|
|
||||||
else if (modelData.name.includes("hdmi"))
|
|
||||||
return "tv"
|
|
||||||
else if (modelData.name.includes("usb"))
|
|
||||||
return "headset"
|
|
||||||
else
|
|
||||||
return "speaker"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: AudioService.displayName(modelData)
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (AudioService.subtitle(modelData.name)
|
|
||||||
&& AudioService.subtitle(
|
|
||||||
modelData.name) !== "")
|
|
||||||
return AudioService.subtitle(modelData.name)
|
|
||||||
+ (modelData === AudioService.sink ? " • Selected" : "")
|
|
||||||
else
|
|
||||||
return modelData === AudioService.sink ? "Selected" : ""
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: text !== ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: deviceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData)
|
|
||||||
Pipewire.preferredDefaultAudioSink = modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(
|
|
||||||
AudioService.source) : ""
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Input Device"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 35
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
visible: AudioService.source !== null
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "check_circle"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Current: " + (root.currentSourceDisplayName || "None")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
|
|
||||||
return []
|
|
||||||
|
|
||||||
let sources = []
|
|
||||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
|
||||||
let node = Pipewire.nodes.values[i]
|
|
||||||
if (!node || node.isStream)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource
|
|
||||||
&& !node.name.includes(".monitor"))
|
|
||||||
sources.push(node)
|
|
||||||
}
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: sourceArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: modelData === AudioService.source ? Theme.primary : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (modelData.name.includes("bluez"))
|
|
||||||
return "headset_mic"
|
|
||||||
else if (modelData.name.includes("usb"))
|
|
||||||
return "headset_mic"
|
|
||||||
else
|
|
||||||
return "mic"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: AudioService.displayName(modelData)
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (AudioService.subtitle(modelData.name)
|
|
||||||
&& AudioService.subtitle(
|
|
||||||
modelData.name) !== "")
|
|
||||||
return AudioService.subtitle(modelData.name)
|
|
||||||
+ (modelData === AudioService.source ? " • Selected" : "")
|
|
||||||
else
|
|
||||||
return modelData === AudioService.source ? "Selected" : ""
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: text !== ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: sourceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData)
|
|
||||||
Pipewire.preferredDefaultAudioSource = modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property real micLevel: Math.min(100,
|
|
||||||
(AudioService.source
|
|
||||||
&& AudioService.source.audio
|
|
||||||
&& AudioService.source.audio.volume * 100)
|
|
||||||
|| 0)
|
|
||||||
property bool micMuted: (AudioService.source && AudioService.source.audio
|
|
||||||
&& AudioService.source.audio.muted) || false
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Microphone Level"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.micMuted ? "mic_off" : "mic"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: root.micMuted ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (AudioService.source && AudioService.source.audio)
|
|
||||||
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: micSliderContainer
|
|
||||||
|
|
||||||
width: parent.width - 80
|
|
||||||
height: 32
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: micSliderTrack
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 8
|
|
||||||
radius: 4
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: micSliderFill
|
|
||||||
|
|
||||||
width: parent.width * (root.micLevel / 100)
|
|
||||||
height: parent.height
|
|
||||||
radius: parent.radius
|
|
||||||
color: Theme.primary
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.standardDecel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: micHandle
|
|
||||||
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
radius: 9
|
|
||||||
color: Theme.primary
|
|
||||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
|
||||||
border.width: 2
|
|
||||||
x: Math.max(0, Math.min(parent.width - width,
|
|
||||||
micSliderFill.width - width / 2))
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
scale: micMouseArea.containsMouse
|
|
||||||
|| micMouseArea.pressed ? 1.2 : 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: micTooltip
|
|
||||||
|
|
||||||
width: tooltipText.contentWidth + Theme.spacingS * 2
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingXS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: (micMouseArea.containsMouse && !root.micMuted)
|
|
||||||
|| micMouseArea.isDragging
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipText
|
|
||||||
|
|
||||||
text: Math.round(root.micLevel) + "%"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.standard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: micMouseArea
|
|
||||||
|
|
||||||
property bool isDragging: false
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
preventStealing: true
|
|
||||||
onPressed: mouse => {
|
|
||||||
isDragging = true
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(1,
|
|
||||||
mouse.x / micSliderTrack.width))
|
|
||||||
let newMicLevel = Math.round(ratio * 100)
|
|
||||||
if (AudioService.source
|
|
||||||
&& AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = false
|
|
||||||
AudioService.source.audio.volume = newMicLevel / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
isDragging = false
|
|
||||||
}
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (pressed && isDragging) {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
mouse.x / micSliderTrack.width))
|
|
||||||
let newMicLevel = Math.max(
|
|
||||||
0, Math.min(100, Math.round(
|
|
||||||
ratio * 100)))
|
|
||||||
if (AudioService.source
|
|
||||||
&& AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = false
|
|
||||||
AudioService.source.audio.volume = newMicLevel / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(1,
|
|
||||||
mouse.x / micSliderTrack.width))
|
|
||||||
let newMicLevel = Math.round(ratio * 100)
|
|
||||||
if (AudioService.source
|
|
||||||
&& AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = false
|
|
||||||
AudioService.source.audio.volume = newMicLevel / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: micGlobalMouseArea
|
|
||||||
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: root.parent ? root.parent.width : 0
|
|
||||||
height: root.parent ? root.parent.height : 0
|
|
||||||
enabled: micMouseArea.isDragging
|
|
||||||
visible: false
|
|
||||||
preventStealing: true
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (micMouseArea.isDragging) {
|
|
||||||
let globalPos = mapToItem(
|
|
||||||
micSliderTrack, mouse.x, mouse.y)
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
globalPos.x / micSliderTrack.width))
|
|
||||||
let newMicLevel = Math.max(
|
|
||||||
0, Math.min(100, Math.round(
|
|
||||||
ratio * 100)))
|
|
||||||
if (AudioService.source
|
|
||||||
&& AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = false
|
|
||||||
AudioService.source.audio.volume = newMicLevel / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
micMouseArea.isDragging = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "mic"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property real volumeLevel: Math.min(
|
|
||||||
100,
|
|
||||||
(AudioService.sink && AudioService.sink.audio
|
|
||||||
&& AudioService.sink.audio.volume * 100)
|
|
||||||
|| 0)
|
|
||||||
property bool volumeMuted: (AudioService.sink && AudioService.sink.audio
|
|
||||||
&& AudioService.sink.audio.muted) || false
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Volume"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: volumeSlider
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
leftIcon: root.volumeMuted ? "volume_off" : "volume_down"
|
|
||||||
rightIcon: "volume_up"
|
|
||||||
enabled: !root.volumeMuted
|
|
||||||
showValue: true
|
|
||||||
unit: "%"
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService.sink
|
|
||||||
&& AudioService.sink.audio ? AudioService.sink.audio : null
|
|
||||||
function onVolumeChanged() {
|
|
||||||
volumeSlider.value = Math.round(
|
|
||||||
AudioService.sink.audio.volume * 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
value = Math.round(AudioService.sink.audio.volume * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
let leftIconItem = volumeSlider.children[0].children[0]
|
|
||||||
if (leftIconItem) {
|
|
||||||
let mouseArea = Qt.createQmlObject(
|
|
||||||
'import QtQuick; import qs.Services; MouseArea { anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: { if (AudioService.sink && AudioService.sink.audio) AudioService.sink.audio.muted = !AudioService.sink.audio.muted; } }',
|
|
||||||
leftIconItem, "dynamicMouseArea")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
if (AudioService.sink
|
|
||||||
&& AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.muted = false
|
|
||||||
AudioService.sink.audio.volume = newValue / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ControlCenter.Audio
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: audioTab
|
|
||||||
|
|
||||||
property int audioSubTab: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankTabBar {
|
|
||||||
width: parent.width
|
|
||||||
tabHeight: 40
|
|
||||||
currentIndex: audioTab.audioSubTab
|
|
||||||
showIcons: false
|
|
||||||
model: [{
|
|
||||||
"text": "Output"
|
|
||||||
}, {
|
|
||||||
"text": "Input"
|
|
||||||
}]
|
|
||||||
onTabClicked: function (index) {
|
|
||||||
audioTab.audioSubTab = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single Loader that switches between Output and Input
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 48
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: audioTab.audioSubTab === 0 ? outputTabComponent : inputTabComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output Tab Component
|
|
||||||
Component {
|
|
||||||
id: outputTabComponent
|
|
||||||
DankFlickable {
|
|
||||||
clip: true
|
|
||||||
contentHeight: outputColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: outputColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: volumeComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: outputDevicesComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input Tab Component
|
|
||||||
Component {
|
|
||||||
id: inputTabComponent
|
|
||||||
DankFlickable {
|
|
||||||
clip: true
|
|
||||||
contentHeight: inputColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: inputColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: microphoneComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: inputDevicesComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume Control Component
|
|
||||||
Component {
|
|
||||||
id: volumeComponent
|
|
||||||
VolumeControl {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Microphone Control Component
|
|
||||||
Component {
|
|
||||||
id: microphoneComponent
|
|
||||||
MicrophoneControl {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output Devices Component
|
|
||||||
Component {
|
|
||||||
id: outputDevicesComponent
|
|
||||||
AudioDevicesList {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input Devices Component
|
|
||||||
Component {
|
|
||||||
id: inputDevicesComponent
|
|
||||||
AudioInputDevicesList {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Available Devices"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - scanButton.width - parent.spacing
|
|
||||||
- 150 // Spacer to push button right
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: scanButton
|
|
||||||
|
|
||||||
width: Math.max(100, scanText.contentWidth + Theme.spacingL * 2)
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08)
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: scanText
|
|
||||||
|
|
||||||
text: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.discovering ? "Stop Scanning" : "Scan"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: scanArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (BluetoothService.adapter)
|
|
||||||
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: noteColumn.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
|
|
||||||
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g,
|
|
||||||
Theme.warning.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: noteColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "info"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.warning
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Pairing Limitation"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.warning
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Quickshell does not support pairing devices that require pin or confirmation."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.8)
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
if (!BluetoothService.adapter
|
|
||||||
|| !BluetoothService.adapter.discovering
|
|
||||||
|| !Bluetooth.devices)
|
|
||||||
return []
|
|
||||||
|
|
||||||
var filtered = Bluetooth.devices.values.filter(dev => {
|
|
||||||
return dev
|
|
||||||
&& !dev.paired
|
|
||||||
&& !dev.pairing
|
|
||||||
&& !dev.blocked
|
|
||||||
&& (dev.signalStrength === undefined
|
|
||||||
|| dev.signalStrength > 0)
|
|
||||||
})
|
|
||||||
return BluetoothService.sortDevices(filtered)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool canConnect: BluetoothService.canConnect(modelData)
|
|
||||||
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 70
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (availableDeviceArea.containsMouse && !isBusy)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08)
|
|
||||||
|
|
||||||
if (modelData.pairing
|
|
||||||
|| modelData.state === BluetoothDeviceState.Connecting)
|
|
||||||
return Qt.rgba(Theme.warning.r, Theme.warning.g,
|
|
||||||
Theme.warning.b, 0.12)
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Qt.rgba(Theme.error.r, Theme.error.g,
|
|
||||||
Theme.error.b, 0.08)
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BluetoothService.getDeviceIcon(modelData)
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name || modelData.deviceName
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
font.weight: modelData.pairing ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return "Pairing..."
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return "Blocked"
|
|
||||||
|
|
||||||
return BluetoothService.getSignalStrength(
|
|
||||||
modelData)
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BluetoothService.getSignalIcon(modelData)
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: modelData.signalStrength !== undefined
|
|
||||||
&& modelData.signalStrength > 0
|
|
||||||
&& !modelData.pairing
|
|
||||||
&& !modelData.blocked
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (modelData.signalStrength !== undefined
|
|
||||||
&& modelData.signalStrength
|
|
||||||
> 0) ? modelData.signalStrength + "%" : ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
visible: modelData.signalStrength !== undefined
|
|
||||||
&& modelData.signalStrength > 0
|
|
||||||
&& !modelData.pairing
|
|
||||||
&& !modelData.blocked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 80
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: modelData.state !== BluetoothDeviceState.Connecting
|
|
||||||
color: {
|
|
||||||
if (!canConnect && !isBusy)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
|
|
||||||
if (actionButtonArea.containsMouse && !isBusy)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
border.color: canConnect || isBusy ? Theme.primary : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
opacity: canConnect || isBusy ? 1 : 0.5
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return "Pairing..."
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return "Blocked"
|
|
||||||
|
|
||||||
return "Connect"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: canConnect || isBusy ? Theme.primary : Qt.rgba(
|
|
||||||
Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: actionButtonArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: canConnect
|
|
||||||
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
|
||||||
enabled: canConnect && !isBusy
|
|
||||||
onClicked: {
|
|
||||||
if (modelData)
|
|
||||||
BluetoothService.connectDeviceWithTrust(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: availableDeviceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 90
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: canConnect
|
|
||||||
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
|
||||||
enabled: canConnect && !isBusy
|
|
||||||
onClicked: {
|
|
||||||
if (modelData)
|
|
||||||
BluetoothService.connectDeviceWithTrust(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: {
|
|
||||||
if (!BluetoothService.adapter
|
|
||||||
|| !BluetoothService.adapter.discovering
|
|
||||||
|| !Bluetooth.devices)
|
|
||||||
return false
|
|
||||||
|
|
||||||
var availableCount = Bluetooth.devices.values.filter(dev => {
|
|
||||||
return dev
|
|
||||||
&& !dev.paired
|
|
||||||
&& !dev.pairing
|
|
||||||
&& !dev.blocked
|
|
||||||
&& (dev.signalStrength
|
|
||||||
=== undefined
|
|
||||||
|| dev.signalStrength > 0)
|
|
||||||
}).length
|
|
||||||
return availableCount === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "sync"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
|
||||||
running: true
|
|
||||||
loops: Animation.Infinite
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 2000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Scanning for devices..."
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Make sure your device is in pairing mode"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No devices found. Put your device in pairing mode and click Start Scanning."
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: {
|
|
||||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
|
||||||
return true
|
|
||||||
|
|
||||||
var availableCount = Bluetooth.devices.values.filter(dev => {
|
|
||||||
return dev
|
|
||||||
&& !dev.paired
|
|
||||||
&& !dev.pairing
|
|
||||||
&& !dev.blocked
|
|
||||||
&& (dev.signalStrength
|
|
||||||
=== undefined
|
|
||||||
|| dev.signalStrength > 0)
|
|
||||||
}).length
|
|
||||||
return availableCount === 0 && !BluetoothService.adapter.discovering
|
|
||||||
}
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var deviceData: null
|
|
||||||
property bool menuVisible: false
|
|
||||||
property var parentItem
|
|
||||||
property var codecSelector
|
|
||||||
|
|
||||||
function show(x, y) {
|
|
||||||
const menuWidth = 160;
|
|
||||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
|
|
||||||
let finalX = x - menuWidth / 2;
|
|
||||||
let finalY = y;
|
|
||||||
finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth));
|
|
||||||
finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight));
|
|
||||||
root.x = finalX;
|
|
||||||
root.y = finalY;
|
|
||||||
root.visible = true;
|
|
||||||
root.menuVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
root.menuVisible = false;
|
|
||||||
Qt.callLater(() => {
|
|
||||||
root.visible = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 160
|
|
||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
opacity: menuVisible ? 1 : 0
|
|
||||||
scale: menuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.deviceData && root.deviceData.connected ? "link_off" : "link"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.deviceData && root.deviceData.connected ? "Disconnect" : "Connect"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: connectArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (root.deviceData) {
|
|
||||||
if (root.deviceData.connected)
|
|
||||||
root.deviceData.disconnect();
|
|
||||||
else
|
|
||||||
BluetoothService.connectDeviceWithTrust(root.deviceData);
|
|
||||||
}
|
|
||||||
root.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: codecArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
visible: root.deviceData && BluetoothService.isAudioDevice(root.deviceData) && root.deviceData.connected
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "high_quality"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Audio Codec"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: codecArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
codecSelector.show(root.deviceData);
|
|
||||||
root.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "delete"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Forget Device"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: forgetArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (root.deviceData)
|
|
||||||
root.deviceData.forget();
|
|
||||||
|
|
||||||
root.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: bluetoothToggle.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12) : (BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
|
|
||||||
border.color: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? Theme.primary : "transparent"
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bluetooth"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Bluetooth"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: bluetoothToggle
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (BluetoothService.adapter)
|
|
||||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
function findBluetoothContextMenu() {
|
|
||||||
var p = parent
|
|
||||||
while (p) {
|
|
||||||
if (p.bluetoothContextMenuWindow)
|
|
||||||
return p.bluetoothContextMenuWindow
|
|
||||||
p = p.parent
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Paired Devices"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter(
|
|
||||||
dev => {
|
|
||||||
return dev
|
|
||||||
&& (dev.paired
|
|
||||||
|| dev.trusted)
|
|
||||||
}) : []
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: btDeviceArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BluetoothService.getDeviceIcon(modelData)
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name || modelData.deviceName
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BluetoothDeviceState.toString(modelData.state)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (modelData.batteryAvailable
|
|
||||||
&& modelData.battery > 0)
|
|
||||||
return "• " + Math.round(
|
|
||||||
modelData.battery * 100) + "%"
|
|
||||||
|
|
||||||
var btBattery = BatteryService.bluetoothDevices.find(
|
|
||||||
dev => {
|
|
||||||
return dev.name === (modelData.name
|
|
||||||
|| modelData.deviceName)
|
|
||||||
|| dev.name.toLowerCase(
|
|
||||||
).includes(
|
|
||||||
(modelData.name
|
|
||||||
|| modelData.deviceName).toLowerCase(
|
|
||||||
))
|
|
||||||
|| (modelData.name
|
|
||||||
|| modelData.deviceName).toLowerCase(
|
|
||||||
).includes(
|
|
||||||
dev.name.toLowerCase())
|
|
||||||
})
|
|
||||||
return btBattery ? "• " + btBattery.percentage + "%" : ""
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: btMenuButton
|
|
||||||
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: btMenuButtonArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b,
|
|
||||||
0.08) : "transparent"
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "more_vert"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.6
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: btMenuButtonArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
var contextMenu = root.findBluetoothContextMenu()
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.deviceData = modelData
|
|
||||||
let localPos = btMenuButtonArea.mapToItem(
|
|
||||||
contextMenu.parentItem,
|
|
||||||
btMenuButtonArea.width / 2,
|
|
||||||
btMenuButtonArea.height)
|
|
||||||
contextMenu.show(localPos.x, localPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: btDeviceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 40
|
|
||||||
hoverEnabled: true
|
|
||||||
enabled: !BluetoothService.isDeviceBusy(modelData)
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData.connected)
|
|
||||||
modelData.disconnect()
|
|
||||||
else
|
|
||||||
BluetoothService.connectDeviceWithTrust(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ControlCenter.Bluetooth
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: bluetoothTab
|
|
||||||
|
|
||||||
property alias bluetoothContextMenuWindow: bluetoothContextMenuWindow
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: toggleComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: pairedComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: availableComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothContextMenu {
|
|
||||||
id: bluetoothContextMenuWindow
|
|
||||||
|
|
||||||
parentItem: bluetoothTab
|
|
||||||
codecSelector: codecSelector
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothCodecSelector {
|
|
||||||
id: codecSelector
|
|
||||||
|
|
||||||
parentItem: bluetoothTab
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: bluetoothContextMenuWindow.visible || codecSelector.visible
|
|
||||||
onClicked: {
|
|
||||||
bluetoothContextMenuWindow.hide();
|
|
||||||
codecSelector.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: bluetoothContextMenuWindow.x
|
|
||||||
y: bluetoothContextMenuWindow.y
|
|
||||||
width: bluetoothContextMenuWindow.width
|
|
||||||
height: bluetoothContextMenuWindow.height
|
|
||||||
onClicked: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: toggleComponent
|
|
||||||
|
|
||||||
BluetoothToggle {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: pairedComponent
|
|
||||||
|
|
||||||
PairedDevicesList {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: availableComponent
|
|
||||||
|
|
||||||
AvailableDevicesList {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
120
Modules/ControlCenter/Components/ActionTile.qml
Normal file
120
Modules/ControlCenter/Components/ActionTile.qml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property string text: ""
|
||||||
|
property string secondaryText: ""
|
||||||
|
property bool isActive: false
|
||||||
|
property bool enabled: true
|
||||||
|
property int widgetIndex: 0
|
||||||
|
property var widgetData: null
|
||||||
|
property bool editMode: false
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
width: parent ? parent.width : 200
|
||||||
|
height: 60
|
||||||
|
radius: {
|
||||||
|
if (Theme.cornerRadius === 0) return 0
|
||||||
|
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color _tileBgActive: Theme.primary
|
||||||
|
readonly property color _tileBgInactive:
|
||||||
|
Theme.surfaceContainerHigh
|
||||||
|
readonly property color _tileRingActive:
|
||||||
|
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
||||||
|
|
||||||
|
color: isActive ? _tileBgActive : _tileBgInactive
|
||||||
|
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: isActive ? 1 : 1
|
||||||
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
function hoverTint(base) {
|
||||||
|
const factor = 1.2
|
||||||
|
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: mouseArea.containsMouse ? hoverTint(root.color) : "transparent"
|
||||||
|
opacity: mouseArea.containsMouse ? 0.08 : 0.0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingL + 2
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: isActive ? Theme.primaryContainer : Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - Theme.iconSize - parent.spacing
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
width: parent.width
|
||||||
|
text: root.text
|
||||||
|
style: Typography.Style.Body
|
||||||
|
color: isActive ? Theme.primaryContainer : Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
width: parent.width
|
||||||
|
text: root.secondaryText
|
||||||
|
style: Typography.Style.Caption
|
||||||
|
color: isActive ? Theme.primaryContainer : Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: root.enabled
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on radius {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Modules/ControlCenter/Components/DetailHost.qml
Normal file
86
Modules/ControlCenter/Components/DetailHost.qml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.ControlCenter.Details
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string expandedSection: ""
|
||||||
|
property var expandedWidgetData: null
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
height: 250
|
||||||
|
y: Theme.spacingS
|
||||||
|
active: parent.height > 0
|
||||||
|
property string sectionKey: root.expandedSection
|
||||||
|
sourceComponent: {
|
||||||
|
switch (root.expandedSection) {
|
||||||
|
case "network":
|
||||||
|
case "wifi": return networkDetailComponent
|
||||||
|
case "bluetooth": return bluetoothDetailComponent
|
||||||
|
case "audioOutput": return audioOutputDetailComponent
|
||||||
|
case "audioInput": return audioInputDetailComponent
|
||||||
|
case "battery": return batteryDetailComponent
|
||||||
|
default:
|
||||||
|
if (root.expandedSection.startsWith("diskUsage_")) {
|
||||||
|
return diskUsageDetailComponent
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSectionKeyChanged: {
|
||||||
|
active = false
|
||||||
|
active = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: networkDetailComponent
|
||||||
|
NetworkDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: bluetoothDetailComponent
|
||||||
|
BluetoothDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioOutputDetailComponent
|
||||||
|
AudioOutputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioInputDetailComponent
|
||||||
|
AudioInputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: batteryDetailComponent
|
||||||
|
BatteryDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: diskUsageDetailComponent
|
||||||
|
DiskUsageDetail {
|
||||||
|
currentMountPath: root.expandedWidgetData?.mountPath || "/"
|
||||||
|
instanceId: root.expandedWidgetData?.instanceId || ""
|
||||||
|
|
||||||
|
|
||||||
|
onMountPathChanged: (newMountPath) => {
|
||||||
|
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || []
|
||||||
|
const newWidgets = widgets.map(w => {
|
||||||
|
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
|
||||||
|
const updatedWidget = Object.assign({}, w)
|
||||||
|
updatedWidget.mountPath = newMountPath
|
||||||
|
return updatedWidget
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
})
|
||||||
|
SettingsData.setControlCenterWidgets(newWidgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
236
Modules/ControlCenter/Components/EditControls.qml
Normal file
236
Modules/ControlCenter/Components/EditControls.qml
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var availableWidgets: []
|
||||||
|
|
||||||
|
signal addWidget(string widgetId)
|
||||||
|
signal resetToDefault()
|
||||||
|
signal clearAll()
|
||||||
|
|
||||||
|
height: 48
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
onAddWidget: addWidgetPopup.close()
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: addWidgetPopup
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 400
|
||||||
|
height: 300
|
||||||
|
modal: true
|
||||||
|
focus: true
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
border.width: 1
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "add_circle"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: "Add Widget"
|
||||||
|
style: Typography.Style.Subtitle
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
model: root.availableWidgets
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: 400 - Theme.spacingL * 2
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: modelData.icon
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
width: 400 - Theme.spacingL * 2 - Theme.iconSize - Theme.spacingM * 3 - Theme.iconSize
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: modelData.text
|
||||||
|
style: Typography.Style.Body
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: modelData.description
|
||||||
|
style: Typography.Style.Caption
|
||||||
|
color: Theme.outline
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "add"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: widgetMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
root.addWidget(modelData.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: (parent.width - Theme.spacingS * 2) / 3
|
||||||
|
height: 48
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "add"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: "Add Widget"
|
||||||
|
style: Typography.Style.Button
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: addWidgetPopup.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: (parent.width - Theme.spacingS * 2) / 3
|
||||||
|
height: 48
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
|
||||||
|
border.color: Theme.warning
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "settings_backup_restore"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.warning
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: "Defaults"
|
||||||
|
style: Typography.Style.Button
|
||||||
|
color: Theme.warning
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.resetToDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: (parent.width - Theme.spacingS * 2) / 3
|
||||||
|
height: 48
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||||
|
border.color: Theme.error
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "clear_all"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.error
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: "Reset"
|
||||||
|
style: Typography.Style.Button
|
||||||
|
color: Theme.error
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clearAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
241
Modules/ControlCenter/Components/EditModeOverlay.qml
Normal file
241
Modules/ControlCenter/Components/EditModeOverlay.qml
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool editMode: false
|
||||||
|
property var widgetData: null
|
||||||
|
property int widgetIndex: -1
|
||||||
|
property bool showSizeControls: true
|
||||||
|
property bool isSlider: false
|
||||||
|
|
||||||
|
signal removeWidget(int index)
|
||||||
|
signal toggleWidgetSize(int index)
|
||||||
|
signal moveWidget(int fromIndex, int toIndex)
|
||||||
|
|
||||||
|
// Delete button in top-right
|
||||||
|
Rectangle {
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
radius: 8
|
||||||
|
color: Theme.error
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: -4
|
||||||
|
visible: editMode
|
||||||
|
z: 10
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "close"
|
||||||
|
size: 12
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: root.removeWidget(widgetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size control buttons in bottom-right
|
||||||
|
Row {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: -8
|
||||||
|
spacing: 4
|
||||||
|
visible: editMode && showSizeControls
|
||||||
|
z: 10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
color: (widgetData?.width || 50) === 25 ? Theme.primary : Theme.primaryContainer
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 1
|
||||||
|
visible: !isSlider
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "25"
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: (widgetData?.width || 50) === 25 ? Theme.primaryText : Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||||
|
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||||
|
widgets[widgetIndex].width = 25
|
||||||
|
SettingsData.setControlCenterWidgets(widgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
color: (widgetData?.width || 50) === 50 ? Theme.primary : Theme.primaryContainer
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "50"
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: (widgetData?.width || 50) === 50 ? Theme.primaryText : Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||||
|
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||||
|
widgets[widgetIndex].width = 50
|
||||||
|
SettingsData.setControlCenterWidgets(widgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
color: (widgetData?.width || 50) === 75 ? Theme.primary : Theme.primaryContainer
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 1
|
||||||
|
visible: !isSlider
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "75"
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: (widgetData?.width || 50) === 75 ? Theme.primaryText : Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||||
|
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||||
|
widgets[widgetIndex].width = 75
|
||||||
|
SettingsData.setControlCenterWidgets(widgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
color: (widgetData?.width || 50) === 100 ? Theme.primary : Theme.primaryContainer
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "100"
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: (widgetData?.width || 50) === 100 ? Theme.primaryText : Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||||
|
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||||
|
widgets[widgetIndex].width = 100
|
||||||
|
SettingsData.setControlCenterWidgets(widgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow buttons for reordering in top-left
|
||||||
|
Row {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: 4
|
||||||
|
spacing: 2
|
||||||
|
visible: editMode
|
||||||
|
z: 20
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
radius: 8
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "keyboard_arrow_left"
|
||||||
|
size: 12
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: widgetIndex > 0
|
||||||
|
opacity: enabled ? 1.0 : 0.5
|
||||||
|
onClicked: root.moveWidget(widgetIndex, widgetIndex - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
radius: 8
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "keyboard_arrow_right"
|
||||||
|
size: 12
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: widgetIndex < ((SettingsData.controlCenterWidgets?.length ?? 0) - 1)
|
||||||
|
opacity: enabled ? 1.0 : 0.5
|
||||||
|
onClicked: root.moveWidget(widgetIndex, widgetIndex + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border highlight
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: editMode ? 1 : 0
|
||||||
|
visible: editMode
|
||||||
|
z: -1
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
NumberAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
115
Modules/ControlCenter/Components/HeaderPane.qml
Normal file
115
Modules/ControlCenter/Components/HeaderPane.qml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool powerOptionsExpanded: false
|
||||||
|
property bool editMode: false
|
||||||
|
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
signal lockRequested()
|
||||||
|
signal editModeToggled()
|
||||||
|
|
||||||
|
implicitHeight: 70
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankCircularImage {
|
||||||
|
id: avatarContainer
|
||||||
|
|
||||||
|
width: 60
|
||||||
|
height: 60
|
||||||
|
imageSource: {
|
||||||
|
if (PortalService.profileImage === "")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if (PortalService.profileImage.startsWith("/"))
|
||||||
|
return "file://" + PortalService.profileImage
|
||||||
|
|
||||||
|
return PortalService.profileImage
|
||||||
|
}
|
||||||
|
fallbackIcon: "person"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: UserInfoService.fullName
|
||||||
|
|| UserInfoService.username || "User"
|
||||||
|
style: Typography.Style.Subtitle
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: (UserInfoService.uptime || "Unknown")
|
||||||
|
style: Typography.Style.Caption
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: actionButtonsRow
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.rightMargin: Theme.spacingXS
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
iconName: "lock"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
onClicked: {
|
||||||
|
root.lockRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: root.powerOptionsExpanded ? Theme.primary : Theme.surfaceText
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
onClicked: {
|
||||||
|
root.powerOptionsExpanded = !root.powerOptionsExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
iconName: "settings"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
onClicked: {
|
||||||
|
settingsModal.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
iconName: editMode ? "done" : "edit"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: editMode ? Theme.primary : Theme.surfaceText
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
onClicked: root.editModeToggled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Modules/ControlCenter/Components/PowerButton.qml
Normal file
52
Modules/ControlCenter/Components/PowerButton.qml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property string text: ""
|
||||||
|
|
||||||
|
signal pressed()
|
||||||
|
|
||||||
|
height: 34
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: mouseArea.containsMouse ? Qt.rgba(
|
||||||
|
Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.12) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.5)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.fontSizeSmall
|
||||||
|
color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: root.text
|
||||||
|
style: Typography.Style.Button
|
||||||
|
color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onPressed: root.pressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Modules/ControlCenter/Components/PowerOptionsPane.qml
Normal file
70
Modules/ControlCenter/Components/PowerOptionsPane.qml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool expanded: false
|
||||||
|
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
|
||||||
|
implicitHeight: expanded ? 60 : 0
|
||||||
|
height: implicitHeight
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: root.expanded ? 1 : 0
|
||||||
|
opacity: root.expanded ? 1 : 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: SessionService.hibernateSupported ? Theme.spacingS : Theme.spacingL
|
||||||
|
visible: root.expanded
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "logout"
|
||||||
|
text: "Logout"
|
||||||
|
onPressed: root.powerActionRequested("logout", "Logout", "Are you sure you want to logout?")
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "restart_alt"
|
||||||
|
text: "Restart"
|
||||||
|
onPressed: root.powerActionRequested("reboot", "Restart", "Are you sure you want to restart?")
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "bedtime"
|
||||||
|
text: "Suspend"
|
||||||
|
onPressed: root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend?")
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "ac_unit"
|
||||||
|
text: "Hibernate"
|
||||||
|
visible: SessionService.hibernateSupported
|
||||||
|
onPressed: root.powerActionRequested("hibernate", "Hibernate", "Are you sure you want to hibernate?")
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "power_settings_new"
|
||||||
|
text: "Shutdown"
|
||||||
|
onPressed: root.powerActionRequested("poweroff", "Shutdown", "Are you sure you want to shutdown?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Modules/ControlCenter/Components/Typography.qml
Normal file
46
Modules/ControlCenter/Components/Typography.qml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
enum Style {
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
Body,
|
||||||
|
Caption,
|
||||||
|
Button
|
||||||
|
}
|
||||||
|
|
||||||
|
property int style: Typography.Style.Body
|
||||||
|
|
||||||
|
font.pixelSize: {
|
||||||
|
switch (style) {
|
||||||
|
case Typography.Style.Title: return Theme.fontSizeXLarge
|
||||||
|
case Typography.Style.Subtitle: return Theme.fontSizeLarge
|
||||||
|
case Typography.Style.Body: return Theme.fontSizeMedium
|
||||||
|
case Typography.Style.Caption: return Theme.fontSizeSmall
|
||||||
|
case Typography.Style.Button: return Theme.fontSizeSmall
|
||||||
|
default: return Theme.fontSizeMedium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
font.weight: {
|
||||||
|
switch (style) {
|
||||||
|
case Typography.Style.Title: return Font.Bold
|
||||||
|
case Typography.Style.Subtitle: return Font.Medium
|
||||||
|
case Typography.Style.Body: return Font.Normal
|
||||||
|
case Typography.Style.Caption: return Font.Normal
|
||||||
|
case Typography.Style.Button: return Font.Medium
|
||||||
|
default: return Font.Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color: {
|
||||||
|
switch (style) {
|
||||||
|
case Typography.Style.Caption: return Theme.surfaceVariantText
|
||||||
|
default: return Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
734
Modules/ControlCenter/Components/WidgetGrid.qml
Normal file
734
Modules/ControlCenter/Components/WidgetGrid.qml
Normal file
@@ -0,0 +1,734 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Components
|
||||||
|
import "../utils/layout.js" as LayoutUtils
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool editMode: false
|
||||||
|
property string expandedSection: ""
|
||||||
|
property int expandedWidgetIndex: -1
|
||||||
|
property var model: null
|
||||||
|
property var expandedWidgetData: null
|
||||||
|
|
||||||
|
signal expandClicked(var widgetData, int globalIndex)
|
||||||
|
signal removeWidget(int index)
|
||||||
|
signal moveWidget(int fromIndex, int toIndex)
|
||||||
|
signal toggleWidgetSize(int index)
|
||||||
|
|
||||||
|
spacing: editMode ? Theme.spacingL : Theme.spacingS
|
||||||
|
|
||||||
|
property var currentRowWidgets: []
|
||||||
|
property real currentRowWidth: 0
|
||||||
|
property int expandedRowIndex: -1
|
||||||
|
|
||||||
|
function calculateRowsAndWidgets() {
|
||||||
|
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
property var layoutResult: {
|
||||||
|
const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets]
|
||||||
|
return calculateRowsAndWidgets()
|
||||||
|
}
|
||||||
|
|
||||||
|
onLayoutResultChanged: {
|
||||||
|
expandedRowIndex = layoutResult.expandedRowIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.layoutResult.rows
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: root.width
|
||||||
|
spacing: 0
|
||||||
|
property int rowIndex: index
|
||||||
|
property var rowWidgets: modelData
|
||||||
|
property bool isSliderOnlyRow: {
|
||||||
|
const widgets = rowWidgets || []
|
||||||
|
if (widgets.length === 0) return false
|
||||||
|
return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider")
|
||||||
|
}
|
||||||
|
topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0
|
||||||
|
bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: rowWidgets || []
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property var widgetData: modelData
|
||||||
|
property int globalWidgetIndex: {
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || []
|
||||||
|
for (var i = 0; i < widgets.length; i++) {
|
||||||
|
if (widgets[i].id === modelData.id) {
|
||||||
|
if (modelData.id === "diskUsage") {
|
||||||
|
if (widgets[i].instanceId === modelData.instanceId) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
property int widgetWidth: modelData.width || 50
|
||||||
|
width: {
|
||||||
|
const baseWidth = root.width
|
||||||
|
const spacing = Theme.spacingS
|
||||||
|
if (widgetWidth <= 25) {
|
||||||
|
return (baseWidth - spacing * 3) / 4
|
||||||
|
} else if (widgetWidth <= 50) {
|
||||||
|
return (baseWidth - spacing) / 2
|
||||||
|
} else if (widgetWidth <= 75) {
|
||||||
|
return (baseWidth - spacing * 2) * 0.75
|
||||||
|
} else {
|
||||||
|
return baseWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
height: 60
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: widgetLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
property var widgetData: parent.widgetData
|
||||||
|
property int widgetIndex: parent.globalWidgetIndex
|
||||||
|
property int globalWidgetIndex: parent.globalWidgetIndex
|
||||||
|
property int widgetWidth: parent.widgetWidth
|
||||||
|
|
||||||
|
sourceComponent: {
|
||||||
|
const id = modelData.id || ""
|
||||||
|
if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
|
||||||
|
return compoundPillComponent
|
||||||
|
} else if (id === "volumeSlider") {
|
||||||
|
return audioSliderComponent
|
||||||
|
} else if (id === "brightnessSlider") {
|
||||||
|
return brightnessSliderComponent
|
||||||
|
} else if (id === "inputVolumeSlider") {
|
||||||
|
return inputAudioSliderComponent
|
||||||
|
} else if (id === "battery") {
|
||||||
|
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent
|
||||||
|
} else if (id === "diskUsage") {
|
||||||
|
return diskUsagePillComponent
|
||||||
|
} else {
|
||||||
|
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailHost {
|
||||||
|
width: parent.width
|
||||||
|
height: active ? (250 + Theme.spacingS) : 0
|
||||||
|
property bool active: {
|
||||||
|
if (root.expandedSection === "") return false
|
||||||
|
|
||||||
|
if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) {
|
||||||
|
const expandedInstanceId = root.expandedWidgetData.instanceId
|
||||||
|
return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowIndex === root.expandedRowIndex
|
||||||
|
}
|
||||||
|
visible: active
|
||||||
|
expandedSection: root.expandedSection
|
||||||
|
expandedWidgetData: root.expandedWidgetData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: compoundPillComponent
|
||||||
|
CompoundPill {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
iconName: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi": {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return "sync"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "settings_ethernet"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return NetworkService.wifiSignalIcon
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "wifi_off"
|
||||||
|
}
|
||||||
|
return "wifi_off"
|
||||||
|
}
|
||||||
|
case "bluetooth": {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
const primaryDevice = (() => {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
for (let device of devices) {
|
||||||
|
if (device && device.connected) {
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})()
|
||||||
|
if (primaryDevice) {
|
||||||
|
return BluetoothService.getDeviceIcon(primaryDevice)
|
||||||
|
}
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
case "audioOutput": {
|
||||||
|
if (!AudioService.sink) return "volume_off"
|
||||||
|
let volume = AudioService.sink.audio.volume
|
||||||
|
let muted = AudioService.sink.audio.muted
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
case "audioInput": {
|
||||||
|
if (!AudioService.source) return "mic_off"
|
||||||
|
let muted = AudioService.source.audio.muted
|
||||||
|
return muted ? "mic_off" : "mic"
|
||||||
|
}
|
||||||
|
default: return widgetDef?.icon || "help"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryText: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi": {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "Ethernet"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
|
||||||
|
return NetworkService.currentWifiSSID
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "Not connected"
|
||||||
|
}
|
||||||
|
return "WiFi off"
|
||||||
|
}
|
||||||
|
case "bluetooth": {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "Bluetooth"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter) {
|
||||||
|
return "No adapter"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter.enabled) {
|
||||||
|
return "Disabled"
|
||||||
|
}
|
||||||
|
return "Enabled"
|
||||||
|
}
|
||||||
|
case "audioOutput": return AudioService.sink?.description || "No output device"
|
||||||
|
case "audioInput": return AudioService.source?.description || "No input device"
|
||||||
|
default: return widgetDef?.text || "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secondaryText: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi": {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return "Please wait..."
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "Connected"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "Select network"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
case "bluetooth": {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "No adapters"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "Off"
|
||||||
|
}
|
||||||
|
const primaryDevice = (() => {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
for (let device of devices) {
|
||||||
|
if (device && device.connected) {
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})()
|
||||||
|
if (primaryDevice) {
|
||||||
|
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
|
||||||
|
}
|
||||||
|
return "No devices"
|
||||||
|
}
|
||||||
|
case "audioOutput": {
|
||||||
|
if (!AudioService.sink) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (AudioService.sink.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(AudioService.sink.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
case "audioInput": {
|
||||||
|
if (!AudioService.source) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (AudioService.source.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(AudioService.source.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
default: return widgetDef?.description || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isActive: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi": {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return NetworkService.wifiEnabled
|
||||||
|
}
|
||||||
|
case "bluetooth": return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||||
|
case "audioOutput": return !!(AudioService.sink && !AudioService.sink.audio.muted)
|
||||||
|
case "audioInput": return !!(AudioService.source && !AudioService.source.audio.muted)
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enabled: (widgetDef?.enabled ?? true)
|
||||||
|
onToggled: {
|
||||||
|
if (root.editMode) return
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi": {
|
||||||
|
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
|
||||||
|
NetworkService.toggleWifiRadio()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "bluetooth": {
|
||||||
|
if (BluetoothService.available && BluetoothService.adapter) {
|
||||||
|
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "audioOutput": {
|
||||||
|
if (AudioService.sink && AudioService.sink.audio) {
|
||||||
|
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "audioInput": {
|
||||||
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
|
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExpandClicked: {
|
||||||
|
if (root.editMode) return
|
||||||
|
root.expandClicked(widgetData, widgetIndex)
|
||||||
|
}
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
const id = widgetData.id || ""
|
||||||
|
if (id === "audioOutput") {
|
||||||
|
if (!AudioService.sink || !AudioService.sink.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = AudioService.sink.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
AudioService.sink.audio.muted = false
|
||||||
|
AudioService.sink.audio.volume = newVolume / 100
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
} else if (id === "audioInput") {
|
||||||
|
if (!AudioService.source || !AudioService.source.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = AudioService.source.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
AudioService.source.audio.muted = false
|
||||||
|
AudioService.source.audio.volume = newVolume / 100
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: false
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioSliderComponent
|
||||||
|
Item {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
||||||
|
width: parent.width
|
||||||
|
height: 16
|
||||||
|
|
||||||
|
AudioSliderRow {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 14
|
||||||
|
property color sliderTrackColor: Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: true
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: brightnessSliderComponent
|
||||||
|
Item {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 16
|
||||||
|
|
||||||
|
BrightnessSliderRow {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 14
|
||||||
|
property color sliderTrackColor: Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: true
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: inputAudioSliderComponent
|
||||||
|
Item {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 16
|
||||||
|
|
||||||
|
InputAudioSliderRow {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 14
|
||||||
|
property color sliderTrackColor: Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: true
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: batteryPillComponent
|
||||||
|
BatteryPill {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
|
||||||
|
onExpandClicked: {
|
||||||
|
if (!root.editMode) {
|
||||||
|
root.expandClicked(widgetData, widgetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: false
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: smallBatteryComponent
|
||||||
|
SmallBatteryButton {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (!root.editMode) {
|
||||||
|
root.expandClicked(widgetData, widgetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: false
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: toggleButtonComponent
|
||||||
|
ToggleButton {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
|
||||||
|
case "darkMode": return "contrast"
|
||||||
|
case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
|
||||||
|
case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
||||||
|
default: return widgetDef?.icon || "help"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode": return "Night Mode"
|
||||||
|
case "darkMode": return SessionData.isLightMode ? "Light Mode" : "Dark Mode"
|
||||||
|
case "doNotDisturb": return "Do Not Disturb"
|
||||||
|
case "idleInhibitor": return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake"
|
||||||
|
default: return widgetDef?.text || "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: ""
|
||||||
|
|
||||||
|
iconRotation: widgetData.id === "darkMode" && SessionData.isLightMode ? 180 : 0
|
||||||
|
|
||||||
|
isActive: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode": return DisplayService.nightModeEnabled || false
|
||||||
|
case "darkMode": return !SessionData.isLightMode
|
||||||
|
case "doNotDisturb": return SessionData.doNotDisturb || false
|
||||||
|
case "idleInhibitor": return SessionService.idleInhibited || false
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled: (widgetDef?.enabled ?? true) && !root.editMode
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode": {
|
||||||
|
if (DisplayService.automationAvailable) {
|
||||||
|
DisplayService.toggleNightMode()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "darkMode": {
|
||||||
|
Theme.toggleLightMode()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "doNotDisturb": {
|
||||||
|
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "idleInhibitor": {
|
||||||
|
SessionService.toggleIdleInhibit()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: false
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: smallToggleComponent
|
||||||
|
SmallToggleButton {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
|
||||||
|
case "darkMode": return "contrast"
|
||||||
|
case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
|
||||||
|
case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
||||||
|
default: return widgetDef?.icon || "help"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iconRotation: widgetData.id === "darkMode" && SessionData.isLightMode ? 180 : 0
|
||||||
|
|
||||||
|
isActive: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode": return DisplayService.nightModeEnabled || false
|
||||||
|
case "darkMode": return !SessionData.isLightMode
|
||||||
|
case "doNotDisturb": return SessionData.doNotDisturb || false
|
||||||
|
case "idleInhibitor": return SessionService.idleInhibited || false
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled: (widgetDef?.enabled ?? true) && !root.editMode
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode": {
|
||||||
|
if (DisplayService.automationAvailable) {
|
||||||
|
DisplayService.toggleNightMode()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "darkMode": {
|
||||||
|
Theme.toggleLightMode()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "doNotDisturb": {
|
||||||
|
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "idleInhibitor": {
|
||||||
|
SessionService.toggleIdleInhibit()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: false
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: diskUsagePillComponent
|
||||||
|
DiskUsagePill {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
|
||||||
|
mountPath: widgetData.mountPath || "/"
|
||||||
|
instanceId: widgetData.instanceId || ""
|
||||||
|
|
||||||
|
onExpandClicked: {
|
||||||
|
if (!root.editMode) {
|
||||||
|
root.expandClicked(widgetData, widgetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditModeOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetData: parent.widgetData
|
||||||
|
widgetIndex: parent.widgetIndex
|
||||||
|
showSizeControls: true
|
||||||
|
isSlider: false
|
||||||
|
onRemoveWidget: (index) => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,762 +8,218 @@ import Quickshell.Wayland
|
|||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.ControlCenter
|
import qs.Modules.ControlCenter
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Details
|
||||||
|
import qs.Modules.TopBar
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Components
|
||||||
|
import qs.Modules.ControlCenter.Models
|
||||||
|
import "./utils/state.js" as StateUtils
|
||||||
|
|
||||||
DankPopout {
|
DankPopout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string currentTab: "network"
|
property string expandedSection: ""
|
||||||
property bool powerOptionsExpanded: false
|
property bool powerOptionsExpanded: false
|
||||||
property string triggerSection: "right"
|
property string triggerSection: "right"
|
||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
|
property bool editMode: false
|
||||||
function setTriggerPosition(x, y, width, section, screen) {
|
property int expandedWidgetIndex: -1
|
||||||
triggerX = x
|
property var expandedWidgetData: null
|
||||||
triggerY = y
|
|
||||||
triggerWidth = width
|
|
||||||
triggerSection = section
|
|
||||||
triggerScreen = screen
|
|
||||||
}
|
|
||||||
|
|
||||||
function openWithTab(tab) {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
close()
|
|
||||||
} else {
|
|
||||||
currentTab = tab
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signal powerActionRequested(string action, string title, string message)
|
signal powerActionRequested(string action, string title, string message)
|
||||||
signal lockRequested
|
signal lockRequested
|
||||||
|
|
||||||
popupWidth: 600
|
function collapseAll() {
|
||||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : (powerOptionsExpanded ? 570 : 500)
|
expandedSection = ""
|
||||||
triggerX: Screen.width - 600 - Theme.spacingL
|
expandedWidgetIndex = -1
|
||||||
|
expandedWidgetData = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditModeChanged: {
|
||||||
|
if (editMode) {
|
||||||
|
collapseAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
collapseAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color _containerBg: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
function setTriggerPosition(x, y, width, section, screen) {
|
||||||
|
StateUtils.setTriggerPosition(root, x, y, width, section, screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWithSection(section) {
|
||||||
|
StateUtils.openWithSection(root, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSection(section) {
|
||||||
|
StateUtils.toggleSection(root, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
popupWidth: 550
|
||||||
|
popupHeight: Math.min((triggerScreen?.height ?? 1080) - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
|
||||||
|
triggerX: (triggerScreen?.width ?? 1920) - 600 - Theme.spacingL
|
||||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
||||||
triggerWidth: 80
|
triggerWidth: 80
|
||||||
positioning: "center"
|
positioning: "center"
|
||||||
WlrLayershell.namespace: "quickshell-controlcenter"
|
|
||||||
screen: triggerScreen
|
screen: triggerScreen
|
||||||
shouldBeVisible: false
|
shouldBeVisible: false
|
||||||
visible: shouldBeVisible
|
visible: shouldBeVisible
|
||||||
|
|
||||||
onShouldBeVisibleChanged: {
|
onShouldBeVisibleChanged: {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
|
Qt.callLater(() => {
|
||||||
if (UserInfoService)
|
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
|
||||||
UserInfoService.getUptime()
|
if (UserInfoService)
|
||||||
|
UserInfoService.getUptime()
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
NetworkService.autoRefreshEnabled = false
|
Qt.callLater(() => {
|
||||||
if (BluetoothService.adapter
|
NetworkService.autoRefreshEnabled = false
|
||||||
&& BluetoothService.adapter.discovering)
|
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
|
||||||
BluetoothService.adapter.discovering = false
|
BluetoothService.adapter.discovering = false
|
||||||
|
editMode = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WidgetModel {
|
||||||
|
id: widgetModel
|
||||||
|
}
|
||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: controlContent
|
id: controlContent
|
||||||
|
|
||||||
implicitHeight: {
|
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
|
||||||
let baseHeight = Theme.spacingL * 2
|
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
||||||
baseHeight += 90 // user header
|
|
||||||
baseHeight += (powerOptionsExpanded ? 60 : 0) + Theme.spacingL // power options
|
|
||||||
baseHeight += 52 + Theme.spacingL // tab bar
|
|
||||||
baseHeight += 280 // tab content area
|
|
||||||
return baseHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
color: Theme.popupBackground()
|
color: {
|
||||||
|
const transparency = Theme.popupTransparency || 0.92
|
||||||
|
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1)
|
||||||
|
return Qt.rgba(surface.r, surface.g, surface.b, transparency)
|
||||||
|
}
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
Theme.outline.b, 0.08)
|
Theme.outline.b, 0.08)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
smooth: true
|
smooth: true
|
||||||
focus: true
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Column {
|
||||||
if (root.shouldBeVisible)
|
id: mainColumn
|
||||||
forceActiveFocus()
|
width: parent.width - Theme.spacingL * 2
|
||||||
}
|
x: Theme.spacingL
|
||||||
|
y: Theme.spacingL
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Keys.onPressed: function (event) {
|
HeaderPane {
|
||||||
if (event.key === Qt.Key_Escape) {
|
id: headerPane
|
||||||
root.close()
|
width: parent.width
|
||||||
event.accepted = true
|
powerOptionsExpanded: root.powerOptionsExpanded
|
||||||
} else {
|
editMode: root.editMode
|
||||||
event.accepted = false
|
onPowerOptionsExpandedChanged: root.powerOptionsExpanded = powerOptionsExpanded
|
||||||
|
onEditModeToggled: root.editMode = !root.editMode
|
||||||
|
onPowerActionRequested: (action, title, message) => root.powerActionRequested(action, title, message)
|
||||||
|
onLockRequested: {
|
||||||
|
root.close()
|
||||||
|
root.lockRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerOptionsPane {
|
||||||
|
id: powerOptionsPane
|
||||||
|
width: parent.width
|
||||||
|
expanded: root.powerOptionsExpanded
|
||||||
|
onPowerActionRequested: (action, title, message) => {
|
||||||
|
root.powerOptionsExpanded = false
|
||||||
|
root.close()
|
||||||
|
root.powerActionRequested(action, title, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetGrid {
|
||||||
|
id: widgetGrid
|
||||||
|
width: parent.width
|
||||||
|
editMode: root.editMode
|
||||||
|
expandedSection: root.expandedSection
|
||||||
|
expandedWidgetIndex: root.expandedWidgetIndex
|
||||||
|
expandedWidgetData: root.expandedWidgetData
|
||||||
|
model: widgetModel
|
||||||
|
onExpandClicked: (widgetData, globalIndex) => {
|
||||||
|
root.expandedWidgetIndex = globalIndex
|
||||||
|
root.expandedWidgetData = widgetData
|
||||||
|
if (widgetData.id === "diskUsage") {
|
||||||
|
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"))
|
||||||
|
} else {
|
||||||
|
root.toggleSection(widgetData.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onRemoveWidget: (index) => widgetModel.removeWidget(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
|
||||||
|
onToggleWidgetSize: (index) => widgetModel.toggleWidgetSize(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
EditControls {
|
||||||
|
width: parent.width
|
||||||
|
visible: editMode
|
||||||
|
availableWidgets: {
|
||||||
|
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id)
|
||||||
|
return widgetModel.baseWidgetDefinitions.filter(w => w.allowMultiple || !existingIds.includes(w.id))
|
||||||
|
}
|
||||||
|
onAddWidget: (widgetId) => widgetModel.addWidget(widgetId)
|
||||||
|
onResetToDefault: () => widgetModel.resetToDefault()
|
||||||
|
onClearAll: () => widgetModel.clearAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
BluetoothCodecSelector {
|
||||||
function onShouldBeVisibleChanged() {
|
id: bluetoothCodecSelector
|
||||||
if (root.shouldBeVisible)
|
|
||||||
Qt.callLater(function () {
|
|
||||||
controlContent.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingL
|
z: 10000
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 90
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha() * 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: avatarContainer
|
|
||||||
|
|
||||||
property bool hasImage: profileImageLoader.status === Image.Ready
|
|
||||||
|
|
||||||
width: 64
|
|
||||||
height: 64
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 1 // The ring is 1px thick.
|
|
||||||
visible: parent.hasImage
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: profileImageLoader
|
|
||||||
|
|
||||||
source: {
|
|
||||||
if (PortalService.profileImage === "")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
if (PortalService.profileImage.startsWith(
|
|
||||||
"/"))
|
|
||||||
return "file://" + PortalService.profileImage
|
|
||||||
|
|
||||||
return PortalService.profileImage
|
|
||||||
}
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
mipmap: true
|
|
||||||
cache: true
|
|
||||||
visible: false // This item is never shown directly.
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
source: profileImageLoader
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: circularMask
|
|
||||||
visible: avatarContainer.hasImage
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: circularMask
|
|
||||||
|
|
||||||
width: 64 - 10
|
|
||||||
height: 64 - 10
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: Theme.primary
|
|
||||||
visible: !parent.hasImage
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "person"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Theme.primaryText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "warning"
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: Theme.primaryText
|
|
||||||
visible: PortalService.profileImage !== ""
|
|
||||||
&& profileImageLoader.status === Image.Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: UserInfoService.fullName
|
|
||||||
|| UserInfoService.username || "User"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Uptime: " + (UserInfoService.uptime
|
|
||||||
|| "Unknown")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 40
|
|
||||||
iconName: "lock"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
backgroundColor: Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
hoverColor: Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
onClicked: {
|
|
||||||
root.close()
|
|
||||||
root.lockRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
radius: 20
|
|
||||||
color: powerButton.containsMouse
|
|
||||||
|| root.powerOptionsExpanded ? Qt.rgba(
|
|
||||||
Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
radius: parent.radius
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: dankIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: powerButton.containsMouse
|
|
||||||
|| root.powerOptionsExpanded ? Theme.error : Theme.surfaceText
|
|
||||||
|
|
||||||
Behavior on name {
|
|
||||||
SequentialAnimation {
|
|
||||||
NumberAnimation {
|
|
||||||
target: dankIcon
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
duration: Theme.shortDuration / 2
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
target: dankIcon
|
|
||||||
property: "name"
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
target: dankIcon
|
|
||||||
property: "opacity"
|
|
||||||
to: 1
|
|
||||||
duration: Theme.shortDuration / 2
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: powerButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = !root.powerOptionsExpanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 40
|
|
||||||
iconName: "settings"
|
|
||||||
iconSize: Theme.iconSize - 2
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
backgroundColor: Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
hoverColor: Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
onClicked: {
|
|
||||||
root.close()
|
|
||||||
settingsModal.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: root.powerOptionsExpanded ? 60 : 0
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
Theme.getContentBackgroundAlpha() * 0.4)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: root.powerOptionsExpanded ? 1 : 0
|
|
||||||
opacity: root.powerOptionsExpanded ? 1 : 0
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
visible: root.powerOptionsExpanded
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 34
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: logoutButton.containsMouse ? Qt.rgba(
|
|
||||||
Theme.warning.r,
|
|
||||||
Theme.warning.g,
|
|
||||||
Theme.warning.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Logout"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = false
|
|
||||||
root.close()
|
|
||||||
root.powerActionRequested(
|
|
||||||
"logout", "Logout",
|
|
||||||
"Are you sure you want to logout?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 34
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: rebootButton.containsMouse ? Qt.rgba(
|
|
||||||
Theme.warning.r,
|
|
||||||
Theme.warning.g,
|
|
||||||
Theme.warning.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "restart_alt"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Restart"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = false
|
|
||||||
root.close()
|
|
||||||
root.powerActionRequested(
|
|
||||||
"reboot", "Restart",
|
|
||||||
"Are you sure you want to restart?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 34
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: suspendButton.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Suspend"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = false
|
|
||||||
root.close()
|
|
||||||
root.powerActionRequested(
|
|
||||||
"suspend", "Suspend",
|
|
||||||
"Are you sure you want to suspend?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 100
|
|
||||||
height: 34
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: shutdownButton.containsMouse ? Qt.rgba(
|
|
||||||
Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.5)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power_settings_new"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Shutdown"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: shutdownButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.powerOptionsExpanded = false
|
|
||||||
root.close()
|
|
||||||
root.powerActionRequested(
|
|
||||||
"poweroff", "Shutdown",
|
|
||||||
"Are you sure you want to shutdown?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: tabBar.height + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.15)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.06)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
DankTabBar {
|
|
||||||
id: tabBar
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
tabHeight: 40
|
|
||||||
currentIndex: {
|
|
||||||
let tabs = ["network", "audio"]
|
|
||||||
if (BluetoothService.available)
|
|
||||||
tabs.push("bluetooth")
|
|
||||||
|
|
||||||
tabs.push("display")
|
|
||||||
return tabs.indexOf(root.currentTab)
|
|
||||||
}
|
|
||||||
model: {
|
|
||||||
let tabs = [{
|
|
||||||
"text": "Network",
|
|
||||||
"icon": "wifi",
|
|
||||||
"id": "network"
|
|
||||||
}]
|
|
||||||
tabs.push({
|
|
||||||
"text": "Audio",
|
|
||||||
"icon": "volume_up",
|
|
||||||
"id": "audio"
|
|
||||||
})
|
|
||||||
if (BluetoothService.available)
|
|
||||||
tabs.push({
|
|
||||||
"text": "Bluetooth",
|
|
||||||
"icon": "bluetooth",
|
|
||||||
"id": "bluetooth"
|
|
||||||
})
|
|
||||||
|
|
||||||
tabs.push({
|
|
||||||
"text": "Display",
|
|
||||||
"icon": "brightness_6",
|
|
||||||
"id": "display"
|
|
||||||
})
|
|
||||||
return tabs
|
|
||||||
}
|
|
||||||
onTabClicked: function (index) {
|
|
||||||
let tabs = ["network", "audio"]
|
|
||||||
if (BluetoothService.available)
|
|
||||||
tabs.push("bluetooth")
|
|
||||||
|
|
||||||
tabs.push("display")
|
|
||||||
root.currentTab = tabs[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.1)
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.05)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: root.currentTab === "network"
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: Component {
|
|
||||||
NetworkTab {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: root.currentTab === "audio"
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: Component {
|
|
||||||
AudioTab {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: BluetoothService.available
|
|
||||||
&& root.currentTab === "bluetooth"
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: Component {
|
|
||||||
BluetoothTab {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: root.currentTab === "display"
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: Component {
|
|
||||||
DisplayTab {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Component {
|
||||||
NumberAnimation {
|
id: networkDetailComponent
|
||||||
duration: Theme.shortDuration
|
NetworkDetail {}
|
||||||
easing.type: Theme.standardEasing
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: bluetoothDetailComponent
|
||||||
|
BluetoothDetail {
|
||||||
|
id: bluetoothDetail
|
||||||
|
onShowCodecSelector: function(device) {
|
||||||
|
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
|
||||||
|
contentLoader.item.bluetoothCodecSelector.show(device)
|
||||||
|
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
|
||||||
|
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioOutputDetailComponent
|
||||||
|
AudioOutputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioInputDetailComponent
|
||||||
|
AudioInputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: batteryDetailComponent
|
||||||
|
BatteryDetail {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
213
Modules/ControlCenter/Details/AudioInputDetail.qml
Normal file
213
Modules/ControlCenter/Details/AudioInputDetail.qml
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool hasInputVolumeSliderInCC: {
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || []
|
||||||
|
return widgets.some(widget => widget.id === "inputVolumeSlider")
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Input Devices"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: volumeSlider
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
height: 35
|
||||||
|
spacing: 0
|
||||||
|
visible: !hasInputVolumeSliderInCC
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
|
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
|
||||||
|
let muted = AudioService.source.audio.muted
|
||||||
|
return muted ? "mic_off" : "mic"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: AudioService.source && AudioService.source.audio && !AudioService.source.audio.muted && AudioService.source.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
readonly property real actualVolumePercent: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
|
enabled: AudioService.source && AudioService.source.audio
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: AudioService.source && AudioService.source.audio ? Math.min(100, Math.round(AudioService.source.audio.volume * 100)) : 0
|
||||||
|
showValue: true
|
||||||
|
unit: "%"
|
||||||
|
valueOverride: actualVolumePercent
|
||||||
|
thumbOutlineColor: Theme.surfaceVariant
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
|
AudioService.source.audio.volume = newValue / 100
|
||||||
|
if (newValue > 0 && AudioService.source.audio.muted) {
|
||||||
|
AudioService.source.audio.muted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: audioContent
|
||||||
|
anchors.top: hasInputVolumeSliderInCC ? headerRow.bottom : volumeSlider.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: hasInputVolumeSliderInCC ? Theme.spacingM : Theme.spacingS
|
||||||
|
contentHeight: audioColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: audioColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
|
return node.audio && !node.isSink && !node.isStream
|
||||||
|
})
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHigh
|
||||||
|
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData === AudioService.source ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (modelData.name.includes("bluez"))
|
||||||
|
return "headset"
|
||||||
|
else if (modelData.name.includes("usb"))
|
||||||
|
return "headset"
|
||||||
|
else
|
||||||
|
return "mic"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: AudioService.displayName(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData === AudioService.source ? "Active" : "Available"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
Pipewire.preferredDefaultAudioSource = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
Modules/ControlCenter/Details/AudioOutputDetail.qml
Normal file
220
Modules/ControlCenter/Details/AudioOutputDetail.qml
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool hasVolumeSliderInCC: {
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || []
|
||||||
|
return widgets.some(widget => widget.id === "volumeSlider")
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Audio Devices"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: volumeSlider
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
height: 35
|
||||||
|
spacing: 0
|
||||||
|
visible: !hasVolumeSliderInCC
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (AudioService.sink && AudioService.sink.audio) {
|
||||||
|
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!AudioService.sink || !AudioService.sink.audio) return "volume_off"
|
||||||
|
let muted = AudioService.sink.audio.muted
|
||||||
|
let volume = AudioService.sink.audio.volume
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
|
enabled: AudioService.sink && AudioService.sink.audio
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0
|
||||||
|
showValue: true
|
||||||
|
unit: "%"
|
||||||
|
valueOverride: actualVolumePercent
|
||||||
|
thumbOutlineColor: Theme.surfaceVariant
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (AudioService.sink && AudioService.sink.audio) {
|
||||||
|
AudioService.sink.audio.volume = newValue / 100
|
||||||
|
if (newValue > 0 && AudioService.sink.audio.muted) {
|
||||||
|
AudioService.sink.audio.muted = false
|
||||||
|
}
|
||||||
|
AudioService.volumeChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: audioContent
|
||||||
|
anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
|
||||||
|
contentHeight: audioColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: audioColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
|
return node.audio && node.isSink && !node.isStream
|
||||||
|
})
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHigh
|
||||||
|
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (modelData.name.includes("bluez"))
|
||||||
|
return "headset"
|
||||||
|
else if (modelData.name.includes("hdmi"))
|
||||||
|
return "tv"
|
||||||
|
else if (modelData.name.includes("usb"))
|
||||||
|
return "headset"
|
||||||
|
else
|
||||||
|
return "speaker"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: AudioService.displayName(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData === AudioService.sink ? "Active" : "Available"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
Pipewire.preferredDefaultAudioSink = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
258
Modules/ControlCenter/Details/BatteryDetail.qml
Normal file
258
Modules/ControlCenter/Details/BatteryDetail.qml
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
function isActiveProfile(profile) {
|
||||||
|
if (typeof PowerProfiles === "undefined") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return PowerProfiles.profile === profile
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProfile(profile) {
|
||||||
|
if (typeof PowerProfiles === "undefined") {
|
||||||
|
ToastService.showError("power-profiles-daemon not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PowerProfiles.profile = profile
|
||||||
|
if (PowerProfiles.profile !== profile) {
|
||||||
|
ToastService.showError("Failed to set power profile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: contentColumn
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BatteryService.getBatteryIcon()
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: {
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
||||||
|
return Theme.error
|
||||||
|
if (BatteryService.isCharging || BatteryService.isPluggedIn)
|
||||||
|
return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - Theme.iconSizeLarge - Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : "Power"
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: {
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||||
|
return Theme.error
|
||||||
|
}
|
||||||
|
if (BatteryService.isCharging) {
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: BatteryService.batteryAvailable ? BatteryService.batteryStatus : "Management"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: {
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||||
|
return Theme.error
|
||||||
|
}
|
||||||
|
if (BatteryService.isCharging) {
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (!BatteryService.batteryAvailable) return "Power profile management available"
|
||||||
|
const time = BatteryService.formatTimeRemaining()
|
||||||
|
if (time !== "Unknown") {
|
||||||
|
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
visible: text.length > 0
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: BatteryService.batteryAvailable
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
height: 64
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Health"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: BatteryService.batteryHealth
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: {
|
||||||
|
if (BatteryService.batteryHealth === "N/A") {
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
const healthNum = parseInt(BatteryService.batteryHealth)
|
||||||
|
return healthNum < 80 ? Theme.error : Theme.surfaceText
|
||||||
|
}
|
||||||
|
font.weight: Font.Bold
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
height: 64
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Capacity"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : "Unknown"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Bold
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButtonGroup {
|
||||||
|
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
||||||
|
property int currentProfileIndex: {
|
||||||
|
if (typeof PowerProfiles === "undefined") return 1
|
||||||
|
return profileModel.findIndex(profile => isActiveProfile(profile))
|
||||||
|
}
|
||||||
|
|
||||||
|
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
|
||||||
|
currentIndex: currentProfileIndex
|
||||||
|
selectionMode: "single"
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected) return
|
||||||
|
setProfile(profileModel[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: parent.width
|
||||||
|
height: degradationContent.implicitHeight + Theme.spacingL * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||||
|
border.color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3)
|
||||||
|
border.width: 1
|
||||||
|
visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: degradationContent
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "warning"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.error
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Profile Degradation"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.error
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8)
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import qs.Common
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Rectangle {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var device: null
|
property var device: null
|
||||||
@@ -15,7 +15,8 @@ Rectangle {
|
|||||||
property var availableCodecs: []
|
property var availableCodecs: []
|
||||||
property string currentCodec: ""
|
property string currentCodec: ""
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
property bool parsingTargetCard: false
|
|
||||||
|
signal codecSelected(string deviceAddress, string codecName)
|
||||||
|
|
||||||
function show(bluetoothDevice) {
|
function show(bluetoothDevice) {
|
||||||
device = bluetoothDevice;
|
device = bluetoothDevice;
|
||||||
@@ -39,75 +40,68 @@ Rectangle {
|
|||||||
|
|
||||||
function queryCodecs() {
|
function queryCodecs() {
|
||||||
if (!device)
|
if (!device)
|
||||||
return ;
|
return;
|
||||||
|
|
||||||
codecQueryProcess.cardName = BluetoothService.getCardName(device);
|
BluetoothService.getAvailableCodecs(device, function(codecs, current) {
|
||||||
codecQueryProcess.running = true;
|
availableCodecs = codecs;
|
||||||
|
currentCodec = current;
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectCodec(profileName) {
|
function selectCodec(profileName) {
|
||||||
if (!device || isLoading)
|
if (!device || isLoading)
|
||||||
return ;
|
return;
|
||||||
|
|
||||||
|
let selectedCodec = availableCodecs.find(c => c.profile === profileName);
|
||||||
|
if (selectedCodec && device) {
|
||||||
|
BluetoothService.updateDeviceCodec(device.address, selectedCodec.name);
|
||||||
|
codecSelected(device.address, selectedCodec.name);
|
||||||
|
}
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
codecSwitchProcess.cardName = BluetoothService.getCardName(device);
|
BluetoothService.switchCodec(device, profileName, function(success, message) {
|
||||||
codecSwitchProcess.profile = profileName;
|
isLoading = false;
|
||||||
codecSwitchProcess.running = true;
|
if (success) {
|
||||||
}
|
ToastService.showToast(message, ToastService.levelInfo);
|
||||||
|
Qt.callLater(root.hide);
|
||||||
function parseCodecLine(line) {
|
} else {
|
||||||
if (!codecQueryProcess.cardName)
|
ToastService.showToast(message, ToastService.levelError);
|
||||||
return ;
|
|
||||||
|
|
||||||
if (line.includes(`Name: ${codecQueryProcess.cardName}`)) {
|
|
||||||
parsingTargetCard = true;
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) {
|
|
||||||
parsingTargetCard = false;
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (parsingTargetCard) {
|
|
||||||
if (line.startsWith("Active Profile:")) {
|
|
||||||
let profile = line.split(": ")[1] || "";
|
|
||||||
let activeCodec = availableCodecs.find((c) => {
|
|
||||||
return c.profile === profile;
|
|
||||||
});
|
|
||||||
if (activeCodec)
|
|
||||||
currentCodec = activeCodec.name;
|
|
||||||
|
|
||||||
return ;
|
|
||||||
}
|
}
|
||||||
if (line.includes("codec") && line.includes("available: yes")) {
|
});
|
||||||
let parts = line.split(": ");
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
let profile = parts[0].trim();
|
|
||||||
let description = parts[1];
|
|
||||||
let codecMatch = description.match(/codec ([^\)\s]+)/i);
|
|
||||||
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN";
|
|
||||||
let codecInfo = BluetoothService.getCodecInfo(codecName);
|
|
||||||
if (codecInfo && !availableCodecs.some((c) => {
|
|
||||||
return c.profile === profile;
|
|
||||||
})) {
|
|
||||||
let newCodecs = availableCodecs.slice();
|
|
||||||
newCodecs.push({
|
|
||||||
"name": codecInfo.name,
|
|
||||||
"profile": profile,
|
|
||||||
"description": codecInfo.description,
|
|
||||||
"qualityColor": codecInfo.qualityColor
|
|
||||||
});
|
|
||||||
availableCodecs = newCodecs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "transparent"
|
|
||||||
z: 2000
|
z: 2000
|
||||||
opacity: modalVisible ? 1 : 0
|
|
||||||
|
MouseArea {
|
||||||
|
id: modalBlocker
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: modalVisible
|
||||||
|
enabled: modalVisible
|
||||||
|
hoverEnabled: true
|
||||||
|
preventStealing: true
|
||||||
|
propagateComposedEvents: false
|
||||||
|
|
||||||
|
onClicked: root.hide()
|
||||||
|
onWheel: (wheel) => { wheel.accepted = true }
|
||||||
|
onPositionChanged: (mouse) => { mouse.accepted = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: modalBackground
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.5)
|
||||||
|
opacity: modalVisible ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: focusScope
|
id: focusScope
|
||||||
@@ -116,20 +110,17 @@ Rectangle {
|
|||||||
focus: root.visible
|
focus: root.visible
|
||||||
enabled: root.visible
|
enabled: root.visible
|
||||||
|
|
||||||
MouseArea {
|
Keys.onEscapePressed: {
|
||||||
anchors.fill: parent
|
root.hide()
|
||||||
onClicked: root.hide()
|
event.accepted = true
|
||||||
onWheel: (wheel) => {
|
|
||||||
return wheel.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: modalContent
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 320
|
width: 320
|
||||||
height: Math.min(contentColumn.implicitHeight + Theme.spacingL * 2, 400)
|
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
@@ -139,8 +130,12 @@ Rectangle {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: {
|
hoverEnabled: true
|
||||||
}
|
preventStealing: true
|
||||||
|
propagateComposedEvents: false
|
||||||
|
onClicked: (mouse) => { mouse.accepted = true }
|
||||||
|
onWheel: (wheel) => { wheel.accepted = true }
|
||||||
|
onPositionChanged: (mouse) => { mouse.accepted = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -309,55 +304,4 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: codecQueryProcess
|
|
||||||
|
|
||||||
property string cardName: ""
|
|
||||||
|
|
||||||
command: ["pactl", "list", "cards"]
|
|
||||||
onExited: function(exitCode, exitStatus) {
|
|
||||||
isLoading = false;
|
|
||||||
if (exitCode !== 0)
|
|
||||||
console.warn("Failed to query codecs:", exitCode);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
onRead: (data) => {
|
|
||||||
return parseCodecLine(data.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: codecSwitchProcess
|
|
||||||
|
|
||||||
property string cardName: ""
|
|
||||||
property string profile: ""
|
|
||||||
|
|
||||||
command: ["pactl", "set-card-profile", cardName, profile]
|
|
||||||
onExited: function(exitCode, exitStatus) {
|
|
||||||
isLoading = false;
|
|
||||||
if (exitCode === 0) {
|
|
||||||
queryCodecs();
|
|
||||||
ToastService.showToast("Codec switched successfully", ToastService.levelInfo);
|
|
||||||
Qt.callLater(root.hide);
|
|
||||||
} else {
|
|
||||||
ToastService.showToast("Failed to switch codec", ToastService.levelError);
|
|
||||||
console.warn("Failed to switch codec:", exitCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
543
Modules/ControlCenter/Details/BluetoothDetail.qml
Normal file
543
Modules/ControlCenter/Details/BluetoothDetail.qml
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
property var bluetoothCodecModalRef: null
|
||||||
|
|
||||||
|
signal showCodecSelector(var device)
|
||||||
|
|
||||||
|
function updateDeviceCodecDisplay(deviceAddress, codecName) {
|
||||||
|
for (let i = 0; i < pairedRepeater.count; i++) {
|
||||||
|
let item = pairedRepeater.itemAt(i)
|
||||||
|
if (item && item.modelData && item.modelData.address === deviceAddress) {
|
||||||
|
item.currentCodec = codecName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Bluetooth Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: scanButton
|
||||||
|
width: 100
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||||
|
return Theme.surfaceContainerHigh
|
||||||
|
return scanMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
|
||||||
|
}
|
||||||
|
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
||||||
|
size: 18
|
||||||
|
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan"
|
||||||
|
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: scanMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: {
|
||||||
|
if (BluetoothService.adapter)
|
||||||
|
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: bluetoothContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
contentHeight: bluetoothColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: bluetoothColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: pairedRepeater
|
||||||
|
model: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
|
return []
|
||||||
|
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
devices.sort((a, b) => {
|
||||||
|
if (a.connected && !b.connected) return -1
|
||||||
|
if (!a.connected && b.connected) return 1
|
||||||
|
return (b.signalStrength || 0) - (a.signalStrength || 0)
|
||||||
|
})
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || ""
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (modelData.connected && BluetoothService.isAudioDevice(modelData)) {
|
||||||
|
BluetoothService.refreshDeviceCodec(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
|
||||||
|
if (deviceMouseArea.containsMouse)
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||||
|
return Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
border.color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
if (modelData.connected)
|
||||||
|
return Theme.primary
|
||||||
|
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
border.width: (modelData.connected || modelData.state === BluetoothDeviceState.Connecting) ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.getDeviceIcon(modelData)
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
if (modelData.connected)
|
||||||
|
return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name || modelData.deviceName || "Unknown Device"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return "Connecting..."
|
||||||
|
if (modelData.connected) {
|
||||||
|
let status = "Connected"
|
||||||
|
if (currentCodec) {
|
||||||
|
status += " • " + currentCodec
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
return "Paired"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
return Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.batteryAvailable && modelData.battery > 0)
|
||||||
|
return "• " + Math.round(modelData.battery * 100) + "%"
|
||||||
|
|
||||||
|
var btBattery = BatteryService.bluetoothDevices.find(dev => {
|
||||||
|
return dev.name === (modelData.name || modelData.deviceName) ||
|
||||||
|
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
|
||||||
|
(modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase())
|
||||||
|
})
|
||||||
|
return btBattery ? "• " + btBattery.percentage + "%" : ""
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: pairedOptionsButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "more_horiz"
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
if (bluetoothContextMenu.visible) {
|
||||||
|
bluetoothContextMenu.close()
|
||||||
|
} else {
|
||||||
|
bluetoothContextMenu.currentDevice = modelData
|
||||||
|
bluetoothContextMenu.popup(pairedOptionsButton, -bluetoothContextMenu.width + pairedOptionsButton.width, pairedOptionsButton.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: pairedOptionsButton.width + Theme.spacingS
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.connected) {
|
||||||
|
modelData.disconnect()
|
||||||
|
} else {
|
||||||
|
BluetoothService.connectDeviceWithTrust(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 80
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "sync"
|
||||||
|
size: 24
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: availableRepeater
|
||||||
|
model: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||||
|
return []
|
||||||
|
|
||||||
|
var filtered = Bluetooth.devices.values.filter(dev => {
|
||||||
|
return dev && !dev.paired && !dev.pairing && !dev.blocked &&
|
||||||
|
(dev.signalStrength === undefined || dev.signalStrength > 0)
|
||||||
|
})
|
||||||
|
return BluetoothService.sortDevices(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property bool canConnect: BluetoothService.canConnect(modelData)
|
||||||
|
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
opacity: canConnect ? 1 : 0.6
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.getDeviceIcon(modelData)
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name || modelData.deviceName || "Unknown Device"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.pairing) return "Pairing..."
|
||||||
|
if (modelData.blocked) return "Blocked"
|
||||||
|
return BluetoothService.getSignalStrength(modelData)
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0 && !modelData.pairing && !modelData.blocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: {
|
||||||
|
if (modelData.pairing) return "Pairing..."
|
||||||
|
if (!canConnect) return "Cannot pair"
|
||||||
|
return "Pair"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: canConnect ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: availableMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: canConnect && !isBusy
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
BluetoothService.connectDeviceWithTrust(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
visible: !BluetoothService.adapter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "No Bluetooth adapter found"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: bluetoothContextMenu
|
||||||
|
width: 150
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
|
property var currentDevice: null
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
|
if (bluetoothContextMenu.currentDevice.connected) {
|
||||||
|
bluetoothContextMenu.currentDevice.disconnect()
|
||||||
|
} else {
|
||||||
|
BluetoothService.connectDeviceWithTrust(bluetoothContextMenu.currentDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Audio Codec"
|
||||||
|
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
|
||||||
|
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
|
showCodecSelector(bluetoothContextMenu.currentDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Forget Device"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
|
bluetoothContextMenu.currentDevice.forget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
169
Modules/ControlCenter/Details/DiskUsageDetail.qml
Normal file
169
Modules/ControlCenter/Details/DiskUsageDetail.qml
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property string currentMountPath: "/"
|
||||||
|
property string instanceId: ""
|
||||||
|
|
||||||
|
signal mountPathChanged(string newMountPath)
|
||||||
|
|
||||||
|
implicitHeight: diskContent.height + Theme.spacingM
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
DgopService.addRef(["diskmounts"])
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
DgopService.removeRef(["diskmounts"])
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: diskContent
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
contentHeight: diskColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: diskColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 100
|
||||||
|
visible: !DgopService.dgopAvailable || !DgopService.diskMounts || DgopService.diskMounts.length === 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: DgopService.dgopAvailable ? "storage" : "error"
|
||||||
|
size: 32
|
||||||
|
color: DgopService.dgopAvailable ? Theme.primary : Theme.error
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: DgopService.dgopAvailable ? "No disk data available" : "dgop not available"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: DgopService.diskMounts || []
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 80
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData.mount === currentMountPath ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "storage"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: {
|
||||||
|
const percentStr = modelData.percent?.replace("%", "") || "0"
|
||||||
|
const percent = parseFloat(percentStr) || 0
|
||||||
|
if (percent > 90) return Theme.error
|
||||||
|
if (percent > 75) return Theme.warning
|
||||||
|
return modelData.mount === currentMountPath ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
const percentStr = modelData.percent?.replace("%", "") || "0"
|
||||||
|
const percent = parseFloat(percentStr) || 0
|
||||||
|
return percent.toFixed(0) + "%"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - 50 - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.mount === "/" ? "Root Filesystem" : modelData.mount
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData.mount === currentMountPath ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.mount
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
visible: modelData.mount !== "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: `${modelData.used || "?"} / ${modelData.size || "?"}`
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.mount !== currentMountPath) {
|
||||||
|
currentMountPath = modelData.mount
|
||||||
|
mountPathChanged(modelData.mount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
476
Modules/ControlCenter/Details/NetworkDetail.qml
Normal file
476
Modules/ControlCenter/Details/NetworkDetail.qml
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return headerRow.height + wifiToggleContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return headerRow.height + wifiContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
return headerRow.height + wifiOffContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
NetworkService.addRef()
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
NetworkService.scanWifi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
NetworkService.removeRef()
|
||||||
|
}
|
||||||
|
|
||||||
|
property var wifiPasswordModalRef: {
|
||||||
|
wifiPasswordModalLoader.active = true
|
||||||
|
return wifiPasswordModalLoader.item
|
||||||
|
}
|
||||||
|
property var networkInfoModalRef: {
|
||||||
|
networkInfoModalLoader.active = true
|
||||||
|
return networkInfoModalLoader.item
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Network Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButtonGroup {
|
||||||
|
id: preferenceControls
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected
|
||||||
|
|
||||||
|
property int currentPreferenceIndex: NetworkService.userPreference === "ethernet" ? 0 : 1
|
||||||
|
|
||||||
|
model: ["Ethernet", "WiFi"]
|
||||||
|
currentIndex: currentPreferenceIndex
|
||||||
|
selectionMode: "single"
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected) return
|
||||||
|
NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: wifiToggleContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: NetworkService.wifiToggling
|
||||||
|
height: visible ? 80 : 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: "sync"
|
||||||
|
size: 32
|
||||||
|
color: Theme.primary
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: NetworkService.wifiToggling
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: wifiOffContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||||
|
height: visible ? 120 : 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: "wifi_off"
|
||||||
|
size: 48
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "WiFi is off"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: 120
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primary
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Enable WiFi"
|
||||||
|
color: Theme.primary
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: enableWifiButton
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: NetworkService.toggleWifiRadio()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: wifiContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||||
|
contentHeight: wifiColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: wifiColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 200
|
||||||
|
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "refresh"
|
||||||
|
size: 48
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
let networks = [...NetworkService.wifiNetworks]
|
||||||
|
networks.sort((a, b) => {
|
||||||
|
if (a.ssid === NetworkService.currentWifiSSID) return -1
|
||||||
|
if (b.ssid === NetworkService.currentWifiSSID) return 1
|
||||||
|
return b.signal - a.signal
|
||||||
|
})
|
||||||
|
return networks
|
||||||
|
}
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHigh
|
||||||
|
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData.ssid === NetworkService.currentWifiSSID ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
let strength = modelData.signal || 0
|
||||||
|
if (strength >= 50) return "wifi"
|
||||||
|
if (strength >= 25) return "wifi_2_bar"
|
||||||
|
return "wifi_1_bar"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.ssid || "Unknown Network"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.saved ? "• Saved" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "• " + modelData.signal + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: optionsButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "more_horiz"
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
if (networkContextMenu.visible) {
|
||||||
|
networkContextMenu.close()
|
||||||
|
} else {
|
||||||
|
networkContextMenu.currentSSID = modelData.ssid
|
||||||
|
networkContextMenu.currentSecured = modelData.secured
|
||||||
|
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID
|
||||||
|
networkContextMenu.currentSaved = modelData.saved
|
||||||
|
networkContextMenu.currentSignal = modelData.signal
|
||||||
|
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: networkMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: optionsButton.width + Theme.spacingS
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: function(event) {
|
||||||
|
if (modelData.ssid !== NetworkService.currentWifiSSID) {
|
||||||
|
if (modelData.secured && !modelData.saved) {
|
||||||
|
if (wifiPasswordModalRef) {
|
||||||
|
wifiPasswordModalRef.show(modelData.ssid)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NetworkService.connectToWifi(modelData.ssid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: networkContextMenu
|
||||||
|
width: 150
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
|
property string currentSSID: ""
|
||||||
|
property bool currentSecured: false
|
||||||
|
property bool currentConnected: false
|
||||||
|
property bool currentSaved: false
|
||||||
|
property int currentSignal: 0
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: networkContextMenu.currentConnected ? "Disconnect" : "Connect"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (networkContextMenu.currentConnected) {
|
||||||
|
NetworkService.disconnectWifi()
|
||||||
|
} else {
|
||||||
|
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
||||||
|
if (wifiPasswordModalRef) {
|
||||||
|
wifiPasswordModalRef.show(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NetworkService.connectToWifi(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Network Info"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (networkInfoModalRef) {
|
||||||
|
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
|
||||||
|
networkInfoModalRef.showNetworkInfo(networkContextMenu.currentSSID, networkData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Forget Network"
|
||||||
|
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
|
||||||
|
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: wifiPasswordModalLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
|
WifiPasswordModal {
|
||||||
|
id: wifiPasswordModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: networkInfoModalLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
|
NetworkInfoModal {
|
||||||
|
id: networkInfoModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: displayTab
|
|
||||||
|
|
||||||
property var brightnessDebounceTimer
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: brightnessComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: settingsComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: brightnessComponent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: BrightnessService.brightnessAvailable
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Brightness"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
id: deviceDropdown
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
visible: BrightnessService.devices.length > 1
|
|
||||||
text: "Device"
|
|
||||||
description: {
|
|
||||||
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
|
|
||||||
if (deviceInfo && deviceInfo.class === "ddc") {
|
|
||||||
return "DDC changes can be slow and unreliable"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
currentValue: BrightnessService.currentDevice
|
|
||||||
options: BrightnessService.devices.map(function (d) {
|
|
||||||
return d.name
|
|
||||||
})
|
|
||||||
optionIcons: BrightnessService.devices.map(function (d) {
|
|
||||||
if (d.class === "backlight")
|
|
||||||
return "desktop_windows"
|
|
||||||
|
|
||||||
if (d.class === "ddc")
|
|
||||||
return "tv"
|
|
||||||
|
|
||||||
if (d.name.includes("kbd"))
|
|
||||||
return "keyboard"
|
|
||||||
|
|
||||||
return "lightbulb"
|
|
||||||
})
|
|
||||||
onValueChanged: function (value) {
|
|
||||||
BrightnessService.setCurrentDevice(value, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: BrightnessService
|
|
||||||
function onDevicesChanged() {
|
|
||||||
if (BrightnessService.currentDevice) {
|
|
||||||
deviceDropdown.currentValue = BrightnessService.currentDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if saved device is now available
|
|
||||||
const lastDevice = SessionData.lastBrightnessDevice
|
|
||||||
|| ""
|
|
||||||
if (lastDevice) {
|
|
||||||
const deviceExists = BrightnessService.devices.some(
|
|
||||||
d => d.name === lastDevice)
|
|
||||||
if (deviceExists
|
|
||||||
&& (!BrightnessService.currentDevice
|
|
||||||
|| BrightnessService.currentDevice !== lastDevice)) {
|
|
||||||
BrightnessService.setCurrentDevice(lastDevice,
|
|
||||||
false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onDeviceSwitched() {
|
|
||||||
// Force update the description when device switches
|
|
||||||
deviceDropdown.description = Qt.binding(function () {
|
|
||||||
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
|
|
||||||
if (deviceInfo && deviceInfo.class === "ddc") {
|
|
||||||
return "DDC changes can be slow and unreliable"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: brightnessSlider
|
|
||||||
width: parent.width
|
|
||||||
value: BrightnessService.brightnessLevel
|
|
||||||
leftIcon: "brightness_low"
|
|
||||||
rightIcon: "brightness_high"
|
|
||||||
enabled: BrightnessService.brightnessAvailable
|
|
||||||
&& BrightnessService.isCurrentDeviceReady()
|
|
||||||
opacity: BrightnessService.isCurrentDeviceReady() ? 1.0 : 0.5
|
|
||||||
onSliderValueChanged: function (newValue) {
|
|
||||||
brightnessDebounceTimer.pendingValue = newValue
|
|
||||||
brightnessDebounceTimer.restart()
|
|
||||||
}
|
|
||||||
onSliderDragFinished: function (finalValue) {
|
|
||||||
brightnessDebounceTimer.stop()
|
|
||||||
BrightnessService.setBrightnessInternal(
|
|
||||||
finalValue, BrightnessService.currentDevice)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: BrightnessService
|
|
||||||
function onBrightnessChanged() {
|
|
||||||
brightnessSlider.value = BrightnessService.brightnessLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeviceSwitched() {
|
|
||||||
brightnessSlider.value = BrightnessService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: settingsComponent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Display Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: BrightnessService.nightModeActive ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: BrightnessService.nightModeActive ? Theme.primary : "transparent"
|
|
||||||
border.width: BrightnessService.nightModeActive ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BrightnessService.nightModeActive ? "nightlight" : "dark_mode"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: BrightnessService.nightModeActive ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Night Mode"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: BrightnessService.nightModeActive ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nightModeToggle
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
BrightnessService.toggleNightMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: SessionData.isLightMode ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: SessionData.isLightMode ? Theme.primary : "transparent"
|
|
||||||
border.width: SessionData.isLightMode ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: SessionData.isLightMode ? "light_mode" : "palette"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: SessionData.isLightMode ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: SessionData.isLightMode ? "Light Mode" : "Dark Mode"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: SessionData.isLightMode ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: lightModeToggle
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
const newLightMode = !SessionData.isLightMode
|
|
||||||
SessionData.setLightMode(newLightMode)
|
|
||||||
Theme.isLightMode = newLightMode
|
|
||||||
PortalService.setLightMode(newLightMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
brightnessDebounceTimer: Timer {
|
|
||||||
property int pendingValue: 0
|
|
||||||
|
|
||||||
interval: {
|
|
||||||
// Use longer interval for DDC devices since ddcutil is slow
|
|
||||||
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
|
|
||||||
return (deviceInfo && deviceInfo.class === "ddc") ? 100 : 50
|
|
||||||
}
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
BrightnessService.setBrightnessInternal(
|
|
||||||
pendingValue, BrightnessService.currentDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
149
Modules/ControlCenter/Models/WidgetModel.qml
Normal file
149
Modules/ControlCenter/Models/WidgetModel.qml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import "../utils/widgets.js" as WidgetUtils
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var baseWidgetDefinitions: [
|
||||||
|
{
|
||||||
|
"id": "nightMode",
|
||||||
|
"text": "Night Mode",
|
||||||
|
"description": "Blue light filter",
|
||||||
|
"icon": "nightlight",
|
||||||
|
"type": "toggle",
|
||||||
|
"enabled": DisplayService.automationAvailable,
|
||||||
|
"warning": !DisplayService.automationAvailable ? "Requires night mode support" : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "darkMode",
|
||||||
|
"text": "Dark Mode",
|
||||||
|
"description": "System theme toggle",
|
||||||
|
"icon": "contrast",
|
||||||
|
"type": "toggle",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "doNotDisturb",
|
||||||
|
"text": "Do Not Disturb",
|
||||||
|
"description": "Block notifications",
|
||||||
|
"icon": "do_not_disturb_on",
|
||||||
|
"type": "toggle",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "idleInhibitor",
|
||||||
|
"text": "Keep Awake",
|
||||||
|
"description": "Prevent screen timeout",
|
||||||
|
"icon": "motion_sensor_active",
|
||||||
|
"type": "toggle",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wifi",
|
||||||
|
"text": "Network",
|
||||||
|
"description": "Wi-Fi and Ethernet connection",
|
||||||
|
"icon": "wifi",
|
||||||
|
"type": "connection",
|
||||||
|
"enabled": NetworkService.wifiAvailable,
|
||||||
|
"warning": !NetworkService.wifiAvailable ? "Wi-Fi not available" : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bluetooth",
|
||||||
|
"text": "Bluetooth",
|
||||||
|
"description": "Device connections",
|
||||||
|
"icon": "bluetooth",
|
||||||
|
"type": "connection",
|
||||||
|
"enabled": BluetoothService.available,
|
||||||
|
"warning": !BluetoothService.available ? "Bluetooth not available" : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audioOutput",
|
||||||
|
"text": "Audio Output",
|
||||||
|
"description": "Speaker settings",
|
||||||
|
"icon": "volume_up",
|
||||||
|
"type": "connection",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audioInput",
|
||||||
|
"text": "Audio Input",
|
||||||
|
"description": "Microphone settings",
|
||||||
|
"icon": "mic",
|
||||||
|
"type": "connection",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "volumeSlider",
|
||||||
|
"text": "Volume Slider",
|
||||||
|
"description": "Audio volume control",
|
||||||
|
"icon": "volume_up",
|
||||||
|
"type": "slider",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "brightnessSlider",
|
||||||
|
"text": "Brightness Slider",
|
||||||
|
"description": "Display brightness control",
|
||||||
|
"icon": "brightness_6",
|
||||||
|
"type": "slider",
|
||||||
|
"enabled": DisplayService.brightnessAvailable,
|
||||||
|
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inputVolumeSlider",
|
||||||
|
"text": "Input Volume Slider",
|
||||||
|
"description": "Microphone volume control",
|
||||||
|
"icon": "mic",
|
||||||
|
"type": "slider",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "battery",
|
||||||
|
"text": "Battery",
|
||||||
|
"description": "Battery and power management",
|
||||||
|
"icon": "battery_std",
|
||||||
|
"type": "action",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diskUsage",
|
||||||
|
"text": "Disk Usage",
|
||||||
|
"description": "Filesystem usage monitoring",
|
||||||
|
"icon": "storage",
|
||||||
|
"type": "action",
|
||||||
|
"enabled": DgopService.dgopAvailable,
|
||||||
|
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined,
|
||||||
|
"allowMultiple": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function getWidgetForId(widgetId) {
|
||||||
|
return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addWidget(widgetId) {
|
||||||
|
WidgetUtils.addWidget(widgetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWidget(index) {
|
||||||
|
WidgetUtils.removeWidget(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWidgetSize(index) {
|
||||||
|
WidgetUtils.toggleWidgetSize(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveWidget(fromIndex, toIndex) {
|
||||||
|
WidgetUtils.moveWidget(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetToDefault() {
|
||||||
|
WidgetUtils.resetToDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
WidgetUtils.clearAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: ethernetCard
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (ethernetPreferenceArea.containsMouse
|
|
||||||
&& NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus !== "ethernet")
|
|
||||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.8)
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.5)
|
|
||||||
}
|
|
||||||
border.color: NetworkService.networkStatus
|
|
||||||
=== "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b,
|
|
||||||
0.12)
|
|
||||||
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "lan"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Ethernet"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: NetworkService.networkStatus
|
|
||||||
=== "ethernet" ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP
|
|
||||||
|| "Connected") : "Disconnected"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: ethernetLoadingSpinner
|
|
||||||
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: NetworkService.changingPreference
|
|
||||||
&& NetworkService.targetPreference === "ethernet"
|
|
||||||
z: 10
|
|
||||||
|
|
||||||
RotationAnimation {
|
|
||||||
target: ethernetLoadingSpinner
|
|
||||||
property: "rotation"
|
|
||||||
running: ethernetLoadingSpinner.visible
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: ethernetPreferenceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: (NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus
|
|
||||||
!== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus !== "ethernet"
|
|
||||||
&& !NetworkService.changingNetworkPreference
|
|
||||||
onClicked: {
|
|
||||||
if (NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled) {
|
|
||||||
|
|
||||||
if (NetworkService.networkStatus !== "ethernet")
|
|
||||||
NetworkService.setNetworkPreference("ethernet")
|
|
||||||
else
|
|
||||||
NetworkService.setNetworkPreference("auto")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: wifiCard
|
|
||||||
|
|
||||||
property var refreshTimer
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus !== "wifi")
|
|
||||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.8)
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.5)
|
|
||||||
}
|
|
||||||
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b,
|
|
||||||
0.12)
|
|
||||||
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
|
|
||||||
visible: NetworkService.wifiAvailable
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.right: wifiToggle.left
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: NetworkService.wifiSignalIcon
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!NetworkService.wifiEnabled)
|
|
||||||
return "WiFi is off"
|
|
||||||
else if (NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.currentWifiSSID)
|
|
||||||
return NetworkService.currentWifiSSID || "Connected"
|
|
||||||
else
|
|
||||||
return "Not Connected"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!NetworkService.wifiEnabled)
|
|
||||||
return "Turn on WiFi to see networks"
|
|
||||||
else if (NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.currentWifiSSID)
|
|
||||||
return NetworkService.wifiIP || "Connected"
|
|
||||||
else
|
|
||||||
return "Select a network below"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: wifiLoadingSpinner
|
|
||||||
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.right: wifiToggle.left
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: NetworkService.changingPreference
|
|
||||||
&& NetworkService.targetPreference === "wifi"
|
|
||||||
z: 10
|
|
||||||
|
|
||||||
RotationAnimation {
|
|
||||||
target: wifiLoadingSpinner
|
|
||||||
property: "rotation"
|
|
||||||
running: wifiLoadingSpinner.visible
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: wifiToggle
|
|
||||||
|
|
||||||
checked: NetworkService.wifiEnabled
|
|
||||||
enabled: true
|
|
||||||
toggling: NetworkService.wifiToggling
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: {
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
NetworkService.currentWifiSSID = ""
|
|
||||||
NetworkService.wifiSignalStrength = 100
|
|
||||||
NetworkService.wifiNetworks = []
|
|
||||||
NetworkService.savedWifiNetworks = []
|
|
||||||
NetworkService.connectionStatus = ""
|
|
||||||
NetworkService.connectingSSID = ""
|
|
||||||
NetworkService.isScanning = false
|
|
||||||
NetworkService.refreshNetworkStatus()
|
|
||||||
}
|
|
||||||
NetworkService.toggleWifiRadio()
|
|
||||||
if (refreshTimer)
|
|
||||||
refreshTimer.triggered = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: wifiPreferenceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 60 // Exclude toggle area
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: (NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus
|
|
||||||
!== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus !== "wifi"
|
|
||||||
&& !NetworkService.changingNetworkPreference
|
|
||||||
onClicked: {
|
|
||||||
if (NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled) {
|
|
||||||
|
|
||||||
if (NetworkService.networkStatus !== "wifi")
|
|
||||||
NetworkService.setNetworkPreference("wifi")
|
|
||||||
else
|
|
||||||
NetworkService.setNetworkPreference("auto")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: wifiContextMenuWindow
|
|
||||||
|
|
||||||
property var networkData: null
|
|
||||||
property bool menuVisible: false
|
|
||||||
property var parentItem
|
|
||||||
property var wifiPasswordModalRef
|
|
||||||
property var networkInfoModalRef
|
|
||||||
|
|
||||||
function show(x, y) {
|
|
||||||
const menuWidth = 160
|
|
||||||
wifiContextMenuWindow.visible = true
|
|
||||||
Qt.callLater(() => {
|
|
||||||
const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
let finalX = x - menuWidth / 2
|
|
||||||
let finalY = y + 4
|
|
||||||
finalX = Math.max(
|
|
||||||
Theme.spacingS, Math.min(
|
|
||||||
finalX,
|
|
||||||
parentItem.width - menuWidth - Theme.spacingS))
|
|
||||||
finalY = Math.max(
|
|
||||||
Theme.spacingS, Math.min(
|
|
||||||
finalY,
|
|
||||||
parentItem.height - menuHeight - Theme.spacingS))
|
|
||||||
if (finalY + menuHeight > parentItem.height - Theme.spacingS) {
|
|
||||||
finalY = y - menuHeight - 4
|
|
||||||
finalY = Math.max(Theme.spacingS, finalY)
|
|
||||||
}
|
|
||||||
wifiContextMenuWindow.x = finalX
|
|
||||||
wifiContextMenuWindow.y = finalY
|
|
||||||
wifiContextMenuWindow.menuVisible = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
wifiContextMenuWindow.menuVisible = false
|
|
||||||
Qt.callLater(() => {
|
|
||||||
wifiContextMenuWindow.visible = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 160
|
|
||||||
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
opacity: menuVisible ? 1 : 0
|
|
||||||
scale: menuVisible ? 1 : 0.85
|
|
||||||
Component.onCompleted: {
|
|
||||||
menuVisible = false
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: wifiMenuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: wifiContextMenuWindow.networkData
|
|
||||||
&& wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: wifiContextMenuWindow.networkData
|
|
||||||
&& wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: connectWifiArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (wifiContextMenuWindow.networkData) {
|
|
||||||
if (wifiContextMenuWindow.networkData.connected) {
|
|
||||||
NetworkService.disconnectWifi()
|
|
||||||
} else {
|
|
||||||
if (wifiContextMenuWindow.networkData.saved) {
|
|
||||||
NetworkService.connectToWifi(
|
|
||||||
wifiContextMenuWindow.networkData.ssid)
|
|
||||||
} else if (wifiContextMenuWindow.networkData.secured) {
|
|
||||||
if (wifiPasswordModalRef) {
|
|
||||||
wifiPasswordModalRef.show(
|
|
||||||
wifiContextMenuWindow.networkData.ssid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NetworkService.connectToWifi(
|
|
||||||
wifiContextMenuWindow.networkData.ssid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wifiContextMenuWindow.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
visible: wifiContextMenuWindow.networkData
|
|
||||||
&& (wifiContextMenuWindow.networkData.saved
|
|
||||||
|| wifiContextMenuWindow.networkData.connected)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "delete"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Forget Network"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: forgetWifiArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (wifiContextMenuWindow.networkData)
|
|
||||||
NetworkService.forgetWifiNetwork(
|
|
||||||
wifiContextMenuWindow.networkData.ssid)
|
|
||||||
|
|
||||||
wifiContextMenuWindow.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: infoWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "info"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Network Info"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: infoWifiArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (wifiContextMenuWindow.networkData
|
|
||||||
&& networkInfoModalRef)
|
|
||||||
networkInfoModalRef.showNetworkInfo(
|
|
||||||
wifiContextMenuWindow.networkData.ssid,
|
|
||||||
wifiContextMenuWindow.networkData)
|
|
||||||
|
|
||||||
wifiContextMenuWindow.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var wifiContextMenuWindow
|
|
||||||
property var sortedWifiNetworks
|
|
||||||
property var wifiPasswordModalRef
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 100
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
visible: NetworkService.wifiEnabled
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Available Networks"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 170
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: refreshAreaSpan.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : NetworkService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: refreshIconSpan
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
rotation: NetworkService.isScanning ? refreshIconSpan.rotation : 0
|
|
||||||
|
|
||||||
RotationAnimation {
|
|
||||||
target: refreshIconSpan
|
|
||||||
property: "rotation"
|
|
||||||
running: NetworkService.isScanning
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on rotation {
|
|
||||||
RotationAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: refreshAreaSpan
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!NetworkService.isScanning) {
|
|
||||||
refreshIconSpan.rotation += 30
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 40
|
|
||||||
clip: true
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: spanningNetworksColumn.height
|
|
||||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: event => {
|
|
||||||
let delta = event.pixelDelta.y
|
|
||||||
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
|
|
||||||
let newY = parent.contentY - delta
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: spanningNetworksColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: NetworkService.wifiAvailable
|
|
||||||
&& NetworkService.wifiEnabled ? sortedWifiNetworks : []
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: spanningNetworksColumn.width
|
|
||||||
height: 38
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: networkArea2.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
|
||||||
border.width: modelData.connected ? 1 : 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingXS
|
|
||||||
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: signalIcon2
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
name: NetworkService.wifiSignalIcon
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: signalIcon2.right
|
|
||||||
anchors.leftMargin: Theme.spacingXS
|
|
||||||
anchors.right: rightIcons2.left
|
|
||||||
anchors.rightMargin: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: modelData.ssid
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: {
|
|
||||||
if (modelData.connected)
|
|
||||||
return "Connected"
|
|
||||||
|
|
||||||
if (NetworkService.connectionStatus === "connecting"
|
|
||||||
&& NetworkService.connectingSSID === modelData.ssid)
|
|
||||||
return "Connecting..."
|
|
||||||
|
|
||||||
if (NetworkService.connectionStatus === "invalid_password"
|
|
||||||
&& NetworkService.connectingSSID === modelData.ssid)
|
|
||||||
return "Invalid password"
|
|
||||||
|
|
||||||
if (modelData.saved)
|
|
||||||
return "Saved"
|
|
||||||
+ (modelData.secured ? " • Secured" : " • Open")
|
|
||||||
|
|
||||||
return modelData.secured ? "Secured" : "Open"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall - 1
|
|
||||||
color: {
|
|
||||||
if (NetworkService.connectionStatus === "connecting"
|
|
||||||
&& NetworkService.connectingSSID === modelData.ssid)
|
|
||||||
return Theme.primary
|
|
||||||
|
|
||||||
if (NetworkService.connectionStatus === "invalid_password"
|
|
||||||
&& NetworkService.connectingSSID === modelData.ssid)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
}
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: rightIcons2
|
|
||||||
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "lock"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
visible: modelData.secured
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: wifiMenuButton
|
|
||||||
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
radius: 12
|
|
||||||
color: wifiMenuButtonArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b,
|
|
||||||
0.08) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "more_vert"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.6
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: wifiMenuButtonArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
wifiContextMenuWindow.networkData = modelData
|
|
||||||
let buttonCenter = wifiMenuButtonArea.width / 2
|
|
||||||
let buttonBottom = wifiMenuButtonArea.height
|
|
||||||
let globalPos = wifiMenuButtonArea.mapToItem(
|
|
||||||
wifiContextMenuWindow.parentItem,
|
|
||||||
buttonCenter, buttonBottom)
|
|
||||||
Qt.callLater(() => {
|
|
||||||
wifiContextMenuWindow.show(
|
|
||||||
globalPos.x,
|
|
||||||
globalPos.y)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: networkArea2
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 32 // Exclude menu button area
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData.connected)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (modelData.saved) {
|
|
||||||
NetworkService.connectToWifi(modelData.ssid)
|
|
||||||
} else if (modelData.secured) {
|
|
||||||
if (wifiPasswordModalRef) {
|
|
||||||
wifiPasswordModalRef.show(modelData.ssid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NetworkService.connectToWifi(modelData.ssid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Network
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: networkTab
|
|
||||||
|
|
||||||
property var wifiPasswordModalRef: {
|
|
||||||
wifiPasswordModalLoader.active = true
|
|
||||||
return wifiPasswordModalLoader.item
|
|
||||||
}
|
|
||||||
property var networkInfoModalRef: {
|
|
||||||
networkInfoModalLoader.active = true
|
|
||||||
return networkInfoModalLoader.item
|
|
||||||
}
|
|
||||||
|
|
||||||
property var sortedWifiNetworks: {
|
|
||||||
if (!NetworkService.wifiAvailable || !NetworkService.wifiEnabled) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
var allNetworks = NetworkService.wifiNetworks
|
|
||||||
var savedNetworks = NetworkService.savedWifiNetworks
|
|
||||||
var currentSSID = NetworkService.currentWifiSSID
|
|
||||||
var signalStrength = NetworkService.wifiSignalStrengthStr
|
|
||||||
var refreshTrigger = forceRefresh
|
|
||||||
|
|
||||||
// Force recalculation
|
|
||||||
var networks = [...allNetworks]
|
|
||||||
|
|
||||||
networks.forEach(function (network) {
|
|
||||||
network.connected = (network.ssid === currentSSID)
|
|
||||||
network.saved = savedNetworks.some(function (saved) {
|
|
||||||
return saved.ssid === network.ssid
|
|
||||||
})
|
|
||||||
if (network.connected && signalStrength) {
|
|
||||||
network.signalStrength = signalStrength
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
networks.sort(function (a, b) {
|
|
||||||
if (a.connected && !b.connected)
|
|
||||||
return -1
|
|
||||||
if (!a.connected && b.connected)
|
|
||||||
return 1
|
|
||||||
return b.signal - a.signal
|
|
||||||
})
|
|
||||||
|
|
||||||
return networks
|
|
||||||
}
|
|
||||||
|
|
||||||
property int forceRefresh: 0
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: NetworkService
|
|
||||||
function onNetworksUpdated() {
|
|
||||||
forceRefresh++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
NetworkService.addRef()
|
|
||||||
if (NetworkService.wifiEnabled)
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
NetworkService.removeRef()
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 30
|
|
||||||
clip: true
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: wifiContent.height
|
|
||||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: event => {
|
|
||||||
let delta = event.pixelDelta.y
|
|
||||||
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
|
|
||||||
let newY = parent.contentY - delta
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: wifiContent
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
WiFiCard {}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 30
|
|
||||||
clip: true
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: ethernetContent.height
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: event => {
|
|
||||||
let delta = event.pixelDelta.y
|
|
||||||
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
|
|
||||||
let newY = parent.contentY - delta
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: ethernetContent
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
EthernetCard {}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 100
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
color: "transparent"
|
|
||||||
visible: !NetworkService.wifiEnabled
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
name: "wifi_off"
|
|
||||||
size: 48
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: "WiFi is turned off"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: "Turn on WiFi to see networks"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiNetworksList {
|
|
||||||
wifiContextMenuWindow: wifiContextMenuWindow
|
|
||||||
sortedWifiNetworks: networkTab.sortedWifiNetworks
|
|
||||||
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: NetworkService
|
|
||||||
function onWifiEnabledChanged() {
|
|
||||||
if (NetworkService.wifiEnabled && visible) {
|
|
||||||
// Trigger a scan when WiFi is enabled
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.wifiNetworks.length === 0) {
|
|
||||||
// Scan when tab becomes visible if we don't have networks cached
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiContextMenu {
|
|
||||||
id: wifiContextMenuWindow
|
|
||||||
parentItem: networkTab
|
|
||||||
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
|
|
||||||
networkInfoModalRef: networkTab.networkInfoModalRef
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: wifiContextMenuWindow.visible
|
|
||||||
onClicked: {
|
|
||||||
wifiContextMenuWindow.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: wifiContextMenuWindow.x
|
|
||||||
y: wifiContextMenuWindow.y
|
|
||||||
width: wifiContextMenuWindow.width
|
|
||||||
height: wifiContextMenuWindow.height
|
|
||||||
onClicked: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -81,8 +81,6 @@ PanelWindow {
|
|||||||
iconName: "close"
|
iconName: "close"
|
||||||
iconSize: Theme.iconSize - 4
|
iconSize: Theme.iconSize - 4
|
||||||
iconColor: Theme.surfaceText
|
iconColor: Theme.surfaceText
|
||||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g,
|
|
||||||
Theme.error.b, 0.12)
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
powerMenuVisible = false
|
powerMenuVisible = false
|
||||||
}
|
}
|
||||||
|
|||||||
63
Modules/ControlCenter/Widgets/AudioInputPill.qml
Normal file
63
Modules/ControlCenter/Widgets/AudioInputPill.qml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
CompoundPill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSource: AudioService.source
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSource) return "mic_off"
|
||||||
|
|
||||||
|
let volume = defaultSource.audio.volume
|
||||||
|
let muted = defaultSource.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "mic_off"
|
||||||
|
return "mic"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: defaultSource && !defaultSource.audio.muted
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!defaultSource) {
|
||||||
|
return "No input device"
|
||||||
|
}
|
||||||
|
return defaultSource.description || "Audio Input"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!defaultSource) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (defaultSource.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(defaultSource.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
if (defaultSource && defaultSource.audio) {
|
||||||
|
defaultSource.audio.muted = !defaultSource.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (!defaultSource || !defaultSource.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = defaultSource.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
defaultSource.audio.muted = false
|
||||||
|
defaultSource.audio.volume = newVolume / 100
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
66
Modules/ControlCenter/Widgets/AudioOutputPill.qml
Normal file
66
Modules/ControlCenter/Widgets/AudioOutputPill.qml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
CompoundPill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: defaultSink && !defaultSink.audio.muted
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!defaultSink) {
|
||||||
|
return "No output device"
|
||||||
|
}
|
||||||
|
return defaultSink.description || "Audio Output"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!defaultSink) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (defaultSink.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(defaultSink.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
if (defaultSink && defaultSink.audio) {
|
||||||
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (!defaultSink || !defaultSink.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = defaultSink.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
defaultSink.audio.volume = newVolume / 100
|
||||||
|
AudioService.volumeChanged()
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
83
Modules/ControlCenter/Widgets/AudioSliderRow.qml
Normal file
83
Modules/ControlCenter/Widgets/AudioSliderRow.qml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
property color sliderTrackColor: "transparent"
|
||||||
|
|
||||||
|
height: 40
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: defaultSink !== null
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
|
enabled: defaultSink !== null
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
|
||||||
|
showValue: true
|
||||||
|
unit: "%"
|
||||||
|
valueOverride: actualVolumePercent
|
||||||
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
|
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.surfaceContainerHigh
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.volume = newValue / 100.0
|
||||||
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Modules/ControlCenter/Widgets/BatteryPill.qml
Normal file
48
Modules/ControlCenter/Widgets/BatteryPill.qml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
CompoundPill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
iconName: BatteryService.getBatteryIcon()
|
||||||
|
|
||||||
|
isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!BatteryService.batteryAvailable) {
|
||||||
|
return "No battery"
|
||||||
|
}
|
||||||
|
return "Battery"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!BatteryService.batteryAvailable) {
|
||||||
|
return "Not available"
|
||||||
|
}
|
||||||
|
if (BatteryService.isCharging) {
|
||||||
|
return `${BatteryService.batteryLevel}% • Charging`
|
||||||
|
}
|
||||||
|
if (BatteryService.isPluggedIn) {
|
||||||
|
return `${BatteryService.batteryLevel}% • Plugged in`
|
||||||
|
}
|
||||||
|
return `${BatteryService.batteryLevel}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
iconColor: {
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||||
|
return Theme.error
|
||||||
|
}
|
||||||
|
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
expandClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
73
Modules/ControlCenter/Widgets/BluetoothPill.qml
Normal file
73
Modules/ControlCenter/Widgets/BluetoothPill.qml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
CompoundPill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var primaryDevice: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
for (let device of devices) {
|
||||||
|
if (device && device.connected) {
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
if (primaryDevice) {
|
||||||
|
return BluetoothService.getDeviceIcon(primaryDevice)
|
||||||
|
}
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||||
|
showExpandArea: BluetoothService.available
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "Bluetooth"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter) {
|
||||||
|
return "No adapter"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter.enabled) {
|
||||||
|
return "Disabled"
|
||||||
|
}
|
||||||
|
return "Enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "No adapters"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "Off"
|
||||||
|
}
|
||||||
|
if (primaryDevice) {
|
||||||
|
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
|
||||||
|
}
|
||||||
|
return "No devices"
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
if (BluetoothService.available && BluetoothService.adapter) {
|
||||||
|
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
Modules/ControlCenter/Widgets/BrightnessSliderRow.qml
Normal file
144
Modules/ControlCenter/Widgets/BrightnessSliderRow.qml
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
height: 40
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse
|
||||||
|
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
|
: "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: function(event) {
|
||||||
|
if (DisplayService.devices.length > 1) {
|
||||||
|
if (deviceMenu.visible) {
|
||||||
|
deviceMenu.close()
|
||||||
|
} else {
|
||||||
|
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
||||||
|
|
||||||
|
let brightness = DisplayService.brightnessLevel
|
||||||
|
if (brightness <= 33) return "brightness_low"
|
||||||
|
if (brightness <= 66) return "brightness_medium"
|
||||||
|
return "brightness_high"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
|
enabled: DisplayService.brightnessAvailable
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
value: {
|
||||||
|
let level = DisplayService.brightnessLevel
|
||||||
|
if (level > 100) {
|
||||||
|
let deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||||
|
if (deviceInfo && deviceInfo.max > 0) {
|
||||||
|
return Math.round((level / deviceInfo.max) * 100)
|
||||||
|
}
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
DisplayService.setBrightness(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
|
trackColor: Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: deviceMenu
|
||||||
|
width: 200
|
||||||
|
closePolicy: Popup.CloseOnEscape
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: DisplayService.devices
|
||||||
|
delegate: MenuItem {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property string deviceName: modelData.name || ""
|
||||||
|
property string deviceClass: modelData.class || ""
|
||||||
|
|
||||||
|
text: deviceName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
indicator: Rectangle {
|
||||||
|
visible: DisplayService.currentDevice === parent.deviceName
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
width: 4
|
||||||
|
height: parent.height - Theme.spacingS * 2
|
||||||
|
radius: 2
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font: parent.font
|
||||||
|
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingL
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
DisplayService.setCurrentDevice(deviceName, true)
|
||||||
|
deviceMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
|
||||||
|
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Modules/ControlCenter/Widgets/CompactSlider.qml
Normal file
70
Modules/ControlCenter/Widgets/CompactSlider.qml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property color iconColor: Theme.surfaceText
|
||||||
|
property string labelText: ""
|
||||||
|
property real value: 0.0
|
||||||
|
property real maximumValue: 1.0
|
||||||
|
property real minimumValue: 0.0
|
||||||
|
property bool enabled: true
|
||||||
|
|
||||||
|
signal sliderValueChanged(real value)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 200
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.right: sliderContainer.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.iconColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.labelText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sliderContainer
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
width: 120
|
||||||
|
height: parent.height - Theme.spacingS * 2
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
enabled: root.enabled
|
||||||
|
minimum: Math.round(root.minimumValue * 100)
|
||||||
|
maximum: Math.round(root.maximumValue * 100)
|
||||||
|
value: Math.round(root.value * 100)
|
||||||
|
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
Modules/ControlCenter/Widgets/CompoundPill.qml
Normal file
167
Modules/ControlCenter/Widgets/CompoundPill.qml
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property color iconColor: Theme.surfaceText
|
||||||
|
property string primaryText: ""
|
||||||
|
property string secondaryText: ""
|
||||||
|
property bool expanded: false
|
||||||
|
property bool isActive: false
|
||||||
|
property bool showExpandArea: true
|
||||||
|
|
||||||
|
signal toggled()
|
||||||
|
signal expandClicked()
|
||||||
|
signal wheelEvent(var wheelEvent)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 220
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
function hoverTint(base) {
|
||||||
|
const factor = 1.2
|
||||||
|
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color _containerBg: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
color: _containerBg
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.10)
|
||||||
|
border.width: 1
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
|
readonly property color _labelPrimary: Theme.surfaceText
|
||||||
|
readonly property color _labelSecondary: Theme.surfaceVariantText
|
||||||
|
readonly property color _tileBgActive: Theme.primary
|
||||||
|
readonly property color _tileBgInactive: {
|
||||||
|
const transparency = Theme.popupTransparency || 0.92
|
||||||
|
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1)
|
||||||
|
return Qt.rgba(surface.r, surface.g, surface.b, transparency)
|
||||||
|
}
|
||||||
|
readonly property color _tileRingActive:
|
||||||
|
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
||||||
|
readonly property color _tileRingInactive:
|
||||||
|
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
|
||||||
|
readonly property color _tileIconActive: Theme.primaryContainer
|
||||||
|
readonly property color _tileIconInactive: Theme.primary
|
||||||
|
|
||||||
|
property int _padH: Theme.spacingS
|
||||||
|
property int _tileSize: 48
|
||||||
|
property int _tileRadius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: rightHoverOverlay
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: root.radius
|
||||||
|
z: 0
|
||||||
|
visible: false
|
||||||
|
color: hoverTint(_containerBg)
|
||||||
|
opacity: 0.08
|
||||||
|
antialiasing: true
|
||||||
|
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: row
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: _padH
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconTile
|
||||||
|
z: 1
|
||||||
|
width: _tileSize
|
||||||
|
height: _tileSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: _tileRadius
|
||||||
|
color: isActive ? _tileBgActive : _tileBgInactive
|
||||||
|
border.color: isActive ? _tileRingActive : "transparent"
|
||||||
|
border.width: isActive ? 1 : 0
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: _tileRadius
|
||||||
|
color: hoverTint(iconTile.color)
|
||||||
|
opacity: tileMouse.pressed ? 0.3 : (tileMouse.containsMouse ? 0.2 : 0.0)
|
||||||
|
visible: opacity > 0
|
||||||
|
antialiasing: true
|
||||||
|
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } }
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: isActive ? _tileIconActive : _tileIconInactive
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: tileMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.toggled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: body
|
||||||
|
width: row.width - iconTile.width - row.spacing
|
||||||
|
height: row.height
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.primaryText
|
||||||
|
color: _labelPrimary
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.secondaryText
|
||||||
|
color: _labelSecondary
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
visible: text.length > 0
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: bodyMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: { rightHoverOverlay.visible = true; rightHoverOverlay.opacity = 0.08 }
|
||||||
|
onExited: { rightHoverOverlay.opacity = 0.0; rightHoverOverlay.visible = false }
|
||||||
|
onPressed: rightHoverOverlay.opacity = 0.16
|
||||||
|
onReleased: rightHoverOverlay.opacity = containsMouse ? 0.08 : 0.0
|
||||||
|
onClicked: root.expandClicked()
|
||||||
|
onWheel: function (ev) {
|
||||||
|
root.wheelEvent(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus: true
|
||||||
|
Keys.onPressed: function (ev) {
|
||||||
|
if (ev.key === Qt.Key_Space || ev.key === Qt.Key_Return) { root.toggled(); ev.accepted = true }
|
||||||
|
else if (ev.key === Qt.Key_Right) { root.expandClicked(); ev.accepted = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user