mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Compare commits
276 Commits
v0.0.6
...
cc-editmod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db28879185 | ||
|
|
92bb5b90aa | ||
|
|
a207ed74ff | ||
|
|
9d224113c4 | ||
|
|
934f4b2210 | ||
|
|
a6a41d4de1 | ||
|
|
185ee20f2d | ||
|
|
778b960130 | ||
|
|
184938d10a | ||
|
|
25565af5f9 | ||
|
|
0bd9a4f860 | ||
|
|
fe64dd1dea | ||
|
|
a4a59fd586 | ||
|
|
7ccd2d9418 | ||
|
|
974dd70a06 | ||
|
|
1690e4f63b | ||
|
|
ec75ef468b | ||
|
|
a57c1e6451 | ||
|
|
bdc79a13a9 | ||
|
|
b893694977 | ||
|
|
9be7d44765 | ||
|
|
3f8d8ca379 | ||
|
|
e50c3cceeb | ||
|
|
14e648911d | ||
|
|
3c42a618d4 | ||
|
|
bed2259944 | ||
|
|
7516d44de9 | ||
|
|
e9a61a4f81 | ||
|
|
7a7d7d053a | ||
|
|
2067e44baf | ||
|
|
8e010478c7 | ||
|
|
ec4f0ff2ed | ||
|
|
c04177e45d | ||
|
|
b9b1737639 | ||
|
|
4468e4c6af | ||
|
|
cf5dec733d | ||
|
|
d9b9da4b3d | ||
|
|
d62ef89bc3 | ||
|
|
1b681e68b9 | ||
|
|
d15ee0c29b | ||
|
|
62b7b30754 | ||
|
|
aa52b586d6 | ||
|
|
ca11735c1d | ||
|
|
78683032aa | ||
|
|
bd5064e567 | ||
|
|
3862f0ced5 | ||
|
|
878c1d9385 | ||
|
|
0a5f635a2d | ||
|
|
6a963ed618 | ||
|
|
35f059b1dc | ||
|
|
b7838e497d | ||
|
|
3edfff388c | ||
|
|
0bfaf2a8f8 | ||
|
|
50f6dfa709 | ||
|
|
897f48d151 | ||
|
|
c43f58e1bf | ||
|
|
de532fdeda | ||
|
|
c21896b0df | ||
|
|
1669be4dd9 | ||
|
|
81550229e8 | ||
|
|
f38ffa1de5 | ||
|
|
66b493fb2b | ||
|
|
ceae254160 | ||
|
|
a3b3e49313 | ||
|
|
8fb17f7fd5 | ||
|
|
c464bb3e47 | ||
|
|
bd72cf3adf | ||
|
|
a734d9b497 | ||
|
|
b18814e086 | ||
|
|
74417a18ea | ||
|
|
227081c5c9 | ||
|
|
c3b3edcae8 | ||
|
|
c8f87085a0 | ||
|
|
118375f004 | ||
|
|
babcc66765 | ||
|
|
64c8e79bf2 | ||
|
|
12e8e72bb2 | ||
|
|
72b79c0f51 | ||
|
|
50e440f212 | ||
|
|
7c1c64fefc | ||
|
|
2e4770f4be | ||
|
|
0b2bf0b83d | ||
|
|
1fbb614aa8 | ||
|
|
55a29d8504 | ||
|
|
ffc13f71b7 | ||
|
|
d4816bd174 | ||
|
|
3cac6e5eea | ||
|
|
50bc3db048 | ||
|
|
2b99e61c65 | ||
|
|
306a0c0e2d | ||
|
|
0ba4ee74b5 | ||
|
|
2b5a5115b7 | ||
|
|
8a7e386adb | ||
|
|
6e6412fffc | ||
|
|
bc540056fc | ||
|
|
0055ddbc8d | ||
|
|
3376dc893d | ||
|
|
4a673fe1b5 | ||
|
|
6639f6ead0 | ||
|
|
2ce32e8bb5 | ||
|
|
4d13a3d909 | ||
|
|
691b6da7a7 | ||
|
|
e6265c2f71 | ||
|
|
de9bb43c26 | ||
|
|
1ae9802866 | ||
|
|
0e887408c5 | ||
|
|
88b7657e34 | ||
|
|
5fcffba73e | ||
|
|
542536455b | ||
|
|
97f0acadb6 | ||
|
|
32f3e579e7 | ||
|
|
bc446dabaf | ||
|
|
d8e2a73e0b | ||
|
|
ae6fd42163 | ||
|
|
cc7363eee1 | ||
|
|
38c4b33d15 | ||
|
|
6c81aa0908 | ||
|
|
102cc3c0b6 | ||
|
|
02300024cf | ||
|
|
344761df00 | ||
|
|
c0edaea716 | ||
|
|
75e660f78e | ||
|
|
be4d9cadba | ||
|
|
fdaac6ac8b | ||
|
|
5c540f826e | ||
|
|
55b3bffccd | ||
|
|
0cb2a96b3e | ||
|
|
9f13ac6963 | ||
|
|
19e148f65c | ||
|
|
7d9334916c | ||
|
|
50dedb55ac | ||
|
|
38f61ceae2 | ||
|
|
fff4b8c817 | ||
|
|
0fe76b7575 | ||
|
|
3c4719d430 | ||
|
|
c04e2ae0ff | ||
|
|
fec921df10 | ||
|
|
5186afce06 | ||
|
|
77ea6a9372 | ||
|
|
afb0435b5c | ||
|
|
903ef0cc72 | ||
|
|
e4f86abda9 | ||
|
|
8ee43de145 | ||
|
|
83b29c5ed9 | ||
|
|
cdf4ca3b3b | ||
|
|
a0192e709e | ||
|
|
59c7246989 | ||
|
|
76ee483b27 | ||
|
|
ba6c7ae28c | ||
|
|
07c1677145 | ||
|
|
3db1a2ccf2 | ||
|
|
6d84553da9 | ||
|
|
77bc129dfc | ||
|
|
2c451b9779 | ||
|
|
3c6bb58088 | ||
|
|
0372c44d98 | ||
|
|
8fc40ab12d | ||
|
|
9e1272029e | ||
|
|
27344a47e2 | ||
|
|
c42681b9b9 | ||
|
|
939f47286e | ||
|
|
c8cb5dc146 | ||
|
|
c3759dc542 | ||
|
|
17b49ad1f9 | ||
|
|
63c54b5611 | ||
|
|
988d5c2fc3 | ||
|
|
7a671b586c | ||
|
|
b36da8eba2 | ||
|
|
5b49e36da4 | ||
|
|
2395274714 | ||
|
|
d0cbe689f8 | ||
|
|
4ae157af35 | ||
|
|
fb94dd0c4a | ||
|
|
fb01d1af4b | ||
|
|
9438868706 | ||
|
|
a77f6eb385 | ||
|
|
44ebd6a2ee | ||
|
|
e5083fb3a4 | ||
|
|
5a9c80540e | ||
|
|
5bb489f3c4 | ||
|
|
35ea621a17 | ||
|
|
a4d1b6ad76 | ||
|
|
a67a6c7c1c | ||
|
|
1c7d8a55f3 | ||
|
|
c9d38ec6f3 | ||
|
|
d961c7569b | ||
|
|
70a971ade2 | ||
|
|
5d4ba8b38a | ||
|
|
537b4b3604 | ||
|
|
7105b09dc0 | ||
|
|
e0118987ad | ||
|
|
fca3c1ef96 | ||
|
|
c867090f12 | ||
|
|
ee06338640 | ||
|
|
751aee40a5 | ||
|
|
252cc8f42a | ||
|
|
dc28015313 | ||
|
|
47b6b365a1 | ||
|
|
e2945a6a2a | ||
|
|
3aa8ea55e7 | ||
|
|
2179468caf | ||
|
|
597f748e56 | ||
|
|
ac1d01a190 | ||
|
|
fd623035c7 | ||
|
|
cbcb9a7f89 | ||
|
|
9c2e5561d7 | ||
|
|
9292e15419 | ||
|
|
c78cb3c767 | ||
|
|
3a1a553a7d | ||
|
|
cbf3d0f330 | ||
|
|
9d416ddbd6 | ||
|
|
079faa0d40 | ||
|
|
4c18e6b01d | ||
|
|
fcd818b098 | ||
|
|
cd1c992abd | ||
|
|
cebcb4d5d9 | ||
|
|
68243107ca | ||
|
|
af952f6397 | ||
|
|
7d88f37f90 | ||
|
|
8abc94fd07 | ||
|
|
c23088c98a | ||
|
|
072cbf284e | ||
|
|
f7dfd31256 | ||
|
|
2ef41555a9 | ||
|
|
c9d7641d86 | ||
|
|
5f97fd4f7c | ||
|
|
9ba3bfb4db | ||
|
|
353ee355a3 | ||
|
|
79c21e67dc | ||
|
|
4963658bc2 | ||
|
|
613faea714 | ||
|
|
2d764d8cac | ||
|
|
bd29abc7e3 | ||
|
|
3d3b2726c9 | ||
|
|
0b76171151 | ||
|
|
8d674a4fdc | ||
|
|
68157ca636 | ||
|
|
18484bb9cc | ||
|
|
e02b2580c9 | ||
|
|
0d6dbf5f99 | ||
|
|
ba1125bc00 | ||
|
|
af5094b479 | ||
|
|
4742636eb3 | ||
|
|
c708f57e8f | ||
|
|
54b63a1f5f | ||
|
|
3c3cffd7b6 | ||
|
|
e64124cce3 | ||
|
|
eecbd8c733 | ||
|
|
9e0693ca8c | ||
|
|
998d390161 | ||
|
|
67a16da8c7 | ||
|
|
21867c842f | ||
|
|
f7c45acc26 | ||
|
|
178ccd3634 | ||
|
|
b4e607e2b4 | ||
|
|
3856ce14cd | ||
|
|
d4db8a01fe | ||
|
|
886c6877d5 | ||
|
|
5146dcb3f7 | ||
|
|
73b832eddb | ||
|
|
f5871ab27e | ||
|
|
ae6a1b77c2 | ||
|
|
93da303967 | ||
|
|
2a8087cd27 | ||
|
|
531d6334fb | ||
|
|
21089aa66e | ||
|
|
be42280aca | ||
|
|
123058f770 | ||
|
|
0872499f36 | ||
|
|
cfbeb6062b | ||
|
|
213543acb3 | ||
|
|
5bffb1ba10 | ||
|
|
96db0581d3 | ||
|
|
c4438b3339 | ||
|
|
154b90103e | ||
|
|
809274a294 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -63,6 +63,11 @@ CLAUDE-temp.md
|
||||
niri-colors.generated.kdl
|
||||
ghostty-colors.generated.conf
|
||||
|
||||
# Notepad files (should be in ~/.local/state/DankMaterialShell/)
|
||||
untitled-*.txt
|
||||
file:*
|
||||
notepad-files/
|
||||
|
||||
result
|
||||
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
@@ -96,4 +101,4 @@ go.work.sum
|
||||
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
# .vscode/
|
||||
10
CLAUDE.md
10
CLAUDE.md
@@ -64,8 +64,7 @@ quickshell -p shell.qml
|
||||
qs -p .
|
||||
|
||||
# Code formatting and linting
|
||||
./qmlformat-all.sh # Format all QML files using project script
|
||||
qmlformat -i **/*.qml # Format all QML files in place
|
||||
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
|
||||
qmllint **/*.qml # Lint all QML files for syntax errors
|
||||
```
|
||||
|
||||
@@ -143,7 +142,6 @@ shell.qml # Main entry point (minimal orchestration)
|
||||
- **ProcessList/**: System monitoring with process management and performance metrics
|
||||
- **Dock/**: Application dock with running apps and window management
|
||||
- **Lock/**: Screen lock system with authentication
|
||||
- **CentcomCenter/**: Calendar, weather, and media widgets
|
||||
|
||||
5. **Modals/** - Full-screen overlays (10 files)
|
||||
- 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
|
||||
- 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.
|
||||
- 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**:
|
||||
- **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
|
||||
|
||||
3. **Null-Safe Operations**:
|
||||
- **Do NOT use** `?.` operator (not supported by qmlformat)
|
||||
- **Use** `object && object.property` instead of `object?.property`
|
||||
- **Example**: `activePlayer && activePlayer.trackTitle` instead of `activePlayer?.trackTitle`
|
||||
- **Use** `object?.property`
|
||||
|
||||
4. **Component Structure**:
|
||||
```qml
|
||||
|
||||
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.
|
||||
@@ -50,6 +50,12 @@ Singleton {
|
||||
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 Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
@@ -15,11 +16,17 @@ Singleton {
|
||||
property string wallpaperPath: ""
|
||||
property string wallpaperLastPath: ""
|
||||
property string profileLastPath: ""
|
||||
property bool perMonitorWallpaper: false
|
||||
property var monitorWallpapers: ({})
|
||||
property bool doNotDisturb: false
|
||||
property bool nightModeEnabled: false
|
||||
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
|
||||
@@ -36,8 +43,27 @@ Singleton {
|
||||
property string wallpaperCyclingMode: "interval" // "interval" or "time"
|
||||
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
|
||||
property string wallpaperCyclingTime: "06:00" // HH:mm format
|
||||
property var monitorCyclingSettings: ({})
|
||||
property string lastBrightnessDevice: ""
|
||||
property string notepadContent: ""
|
||||
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: {
|
||||
loadSettings()
|
||||
@@ -53,19 +79,15 @@ Singleton {
|
||||
var settings = JSON.parse(content)
|
||||
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
|
||||
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
|
||||
wallpaperLastPath = settings.wallpaperLastPath
|
||||
!== undefined ? settings.wallpaperLastPath : ""
|
||||
profileLastPath = settings.profileLastPath
|
||||
!== undefined ? settings.profileLastPath : ""
|
||||
wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : ""
|
||||
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : ""
|
||||
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
|
||||
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
|
||||
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
|
||||
nightModeEnabled = settings.nightModeEnabled
|
||||
!== undefined ? settings.nightModeEnabled : false
|
||||
nightModeTemperature = settings.nightModeTemperature
|
||||
!== undefined ? settings.nightModeTemperature : 4500
|
||||
nightModeAutoEnabled = settings.nightModeAutoEnabled
|
||||
!== undefined ? settings.nightModeAutoEnabled : false
|
||||
nightModeAutoMode = settings.nightModeAutoMode
|
||||
!== undefined ? settings.nightModeAutoMode : "time"
|
||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
|
||||
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
|
||||
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
|
||||
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
|
||||
// Handle legacy time format
|
||||
if (settings.nightModeStartTime !== undefined) {
|
||||
const parts = settings.nightModeStartTime.split(":")
|
||||
@@ -87,25 +109,34 @@ Singleton {
|
||||
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
|
||||
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
|
||||
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
|
||||
selectedGpuIndex = settings.selectedGpuIndex
|
||||
!== undefined ? settings.selectedGpuIndex : 0
|
||||
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled
|
||||
!== undefined ? settings.nvidiaGpuTempEnabled : false
|
||||
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled
|
||||
!== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
||||
enabledGpuPciIds = settings.enabledGpuPciIds
|
||||
!== undefined ? settings.enabledGpuPciIds : []
|
||||
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled
|
||||
!== undefined ? settings.wallpaperCyclingEnabled : false
|
||||
wallpaperCyclingMode = settings.wallpaperCyclingMode
|
||||
!== undefined ? settings.wallpaperCyclingMode : "interval"
|
||||
wallpaperCyclingInterval = settings.wallpaperCyclingInterval
|
||||
!== undefined ? settings.wallpaperCyclingInterval : 300
|
||||
wallpaperCyclingTime = settings.wallpaperCyclingTime
|
||||
!== undefined ? settings.wallpaperCyclingTime : "06:00"
|
||||
lastBrightnessDevice = settings.lastBrightnessDevice
|
||||
!== undefined ? settings.lastBrightnessDevice : ""
|
||||
notepadContent = settings.notepadContent !== undefined ? settings.notepadContent : ""
|
||||
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
||||
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
|
||||
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
||||
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
|
||||
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
|
||||
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
|
||||
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
|
||||
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
|
||||
monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {}
|
||||
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
|
||||
launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""
|
||||
wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"
|
||||
includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none")
|
||||
|
||||
acMonitorTimeout = settings.acMonitorTimeout !== undefined ? settings.acMonitorTimeout : 0
|
||||
acLockTimeout = settings.acLockTimeout !== undefined ? settings.acLockTimeout : 0
|
||||
acSuspendTimeout = settings.acSuspendTimeout !== undefined ? settings.acSuspendTimeout : 0
|
||||
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) {
|
||||
|
||||
@@ -118,6 +149,8 @@ Singleton {
|
||||
"wallpaperPath": wallpaperPath,
|
||||
"wallpaperLastPath": wallpaperLastPath,
|
||||
"profileLastPath": profileLastPath,
|
||||
"perMonitorWallpaper": perMonitorWallpaper,
|
||||
"monitorWallpapers": monitorWallpapers,
|
||||
"doNotDisturb": doNotDisturb,
|
||||
"nightModeEnabled": nightModeEnabled,
|
||||
"nightModeTemperature": nightModeTemperature,
|
||||
@@ -139,8 +172,20 @@ Singleton {
|
||||
"wallpaperCyclingMode": wallpaperCyclingMode,
|
||||
"wallpaperCyclingInterval": wallpaperCyclingInterval,
|
||||
"wallpaperCyclingTime": wallpaperCyclingTime,
|
||||
"monitorCyclingSettings": monitorCyclingSettings,
|
||||
"lastBrightnessDevice": lastBrightnessDevice,
|
||||
"notepadContent": notepadContent
|
||||
"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))
|
||||
}
|
||||
|
||||
@@ -218,22 +263,33 @@ Singleton {
|
||||
}
|
||||
|
||||
function setWallpaper(imagePath) {
|
||||
console.log("SessionData.setWallpaper called with:", imagePath)
|
||||
wallpaperPath = imagePath
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
console.log("Theme is available, current theme:", Theme.currentTheme)
|
||||
// Always extract colors for shell UI if dynamic theming is enabled
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.wallpaperDynamicTheming) {
|
||||
console.log("Dynamic theming enabled, extracting colors")
|
||||
Theme.extractColors()
|
||||
}
|
||||
// Always generate system themes (matugen templates) when wallpaper changes
|
||||
console.log("Calling generateSystemThemesFromCurrentTheme")
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
} else {
|
||||
console.log("Theme is undefined!")
|
||||
}
|
||||
}
|
||||
|
||||
function setWallpaperColor(color) {
|
||||
wallpaperPath = color
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function clearWallpaper() {
|
||||
wallpaperPath = ""
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
|
||||
Theme.switchTheme(SettingsData.theme)
|
||||
} else {
|
||||
Theme.switchTheme("blue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,40 +369,204 @@ Singleton {
|
||||
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") {
|
||||
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) {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMonitorWallpaper(screenName) {
|
||||
if (!perMonitorWallpaper) {
|
||||
return wallpaperPath
|
||||
}
|
||||
return monitorWallpapers[screenName] || wallpaperPath
|
||||
}
|
||||
|
||||
function setLastBrightnessDevice(device) {
|
||||
lastBrightnessDevice = device
|
||||
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 {
|
||||
id: settingsFile
|
||||
|
||||
path: StandardPaths.writableLocation(
|
||||
StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
watchChanges: true
|
||||
onLoaded: {
|
||||
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 {
|
||||
target: "wallpaper"
|
||||
|
||||
function get(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead."
|
||||
}
|
||||
return root.wallpaperPath || ""
|
||||
}
|
||||
|
||||
function set(path: string): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead."
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return "ERROR: No path provided"
|
||||
}
|
||||
|
||||
var absolutePath = path.startsWith(
|
||||
"/") ? path : StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/" + path
|
||||
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||
|
||||
try {
|
||||
root.setWallpaper(absolutePath)
|
||||
@@ -358,10 +578,17 @@ Singleton {
|
||||
|
||||
function clear(): string {
|
||||
root.setWallpaper("")
|
||||
return "SUCCESS: Wallpaper cleared"
|
||||
root.setPerMonitorWallpaper(false)
|
||||
root.monitorWallpapers = {}
|
||||
root.saveSettings()
|
||||
return "SUCCESS: All wallpapers cleared"
|
||||
}
|
||||
|
||||
function next(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead."
|
||||
}
|
||||
|
||||
if (!root.wallpaperPath) {
|
||||
return "ERROR: No wallpaper set"
|
||||
}
|
||||
@@ -375,6 +602,10 @@ Singleton {
|
||||
}
|
||||
|
||||
function prev(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead."
|
||||
}
|
||||
|
||||
if (!root.wallpaperPath) {
|
||||
return "ERROR: No wallpaper set"
|
||||
}
|
||||
@@ -386,5 +617,70 @@ Singleton {
|
||||
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function getFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided"
|
||||
}
|
||||
return root.getMonitorWallpaper(screenName) || ""
|
||||
}
|
||||
|
||||
function setFor(screenName: string, path: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided"
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return "ERROR: No path provided"
|
||||
}
|
||||
|
||||
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||
|
||||
try {
|
||||
if (!root.perMonitorWallpaper) {
|
||||
root.setPerMonitorWallpaper(true)
|
||||
}
|
||||
root.setMonitorWallpaper(screenName, absolutePath)
|
||||
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function nextFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided"
|
||||
}
|
||||
|
||||
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||
if (!currentWallpaper) {
|
||||
return "ERROR: No wallpaper set for " + screenName
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cycleNextForMonitor(screenName)
|
||||
return "SUCCESS: Cycling to next wallpaper for " + screenName
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function prevFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided"
|
||||
}
|
||||
|
||||
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||
if (!currentWallpaper) {
|
||||
return "ERROR: No wallpaper set for " + screenName
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cyclePrevForMonitor(screenName)
|
||||
return "SUCCESS: Cycling to previous wallpaper for " + screenName
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
@@ -13,9 +15,10 @@ Singleton {
|
||||
// Theme settings
|
||||
property string currentThemeName: "blue"
|
||||
property string customThemeFile: ""
|
||||
property string matugenScheme: "scheme-tonal-spot"
|
||||
property real topBarTransparency: 0.75
|
||||
property real topBarWidgetTransparency: 0.85
|
||||
property real popupTransparency: 0.92
|
||||
property real popupTransparency: 1.0
|
||||
property real dockTransparency: 1
|
||||
property bool use24HourClock: true
|
||||
property bool useFahrenheit: false
|
||||
@@ -43,9 +46,23 @@ Singleton {
|
||||
property bool controlCenterShowNetworkIcon: true
|
||||
property bool controlCenterShowBluetoothIcon: 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 showWorkspacePadding: false
|
||||
property bool showWorkspaceApps: false
|
||||
property int maxWorkspaceIcons: 3
|
||||
property bool workspacesPerMonitor: true
|
||||
property var workspaceNameIcons: ({})
|
||||
property bool waveProgressEnabled: true
|
||||
property bool clockCompactMode: false
|
||||
property bool focusedWindowCompactMode: false
|
||||
property bool runningAppsCompactMode: true
|
||||
@@ -72,42 +89,65 @@ Singleton {
|
||||
property string osLogoColorOverride: ""
|
||||
property real osLogoBrightness: 0.5
|
||||
property real osLogoContrast: 1
|
||||
property bool wallpaperDynamicTheming: true
|
||||
property bool weatherEnabled: true
|
||||
property string fontFamily: "Inter Variable"
|
||||
property string monoFontFamily: "Fira Code"
|
||||
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 qtThemingEnabled: false
|
||||
property bool showDock: false
|
||||
property bool dockAutoHide: false
|
||||
property bool dockGroupByApp: false
|
||||
property real cornerRadius: 12
|
||||
property bool notificationOverlayEnabled: false
|
||||
property bool topBarAutoHide: false
|
||||
property bool topBarOpenOnOverview: false
|
||||
property bool topBarVisible: true
|
||||
property real topBarSpacing: 4
|
||||
property real topBarBottomGap: 0
|
||||
property real topBarInnerPadding: 8
|
||||
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: "s"
|
||||
property int notificationTimeoutLow: 5000
|
||||
property int notificationTimeoutNormal: 5000
|
||||
property int notificationTimeoutCritical: 0
|
||||
property var screenPreferences: ({})
|
||||
readonly property string defaultFontFamily: "Inter Variable"
|
||||
readonly property string defaultMonoFontFamily: "Fira Code"
|
||||
readonly property string _homeUrl: StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation)
|
||||
readonly property string _configUrl: StandardPaths.writableLocation(
|
||||
StandardPaths.ConfigLocation)
|
||||
readonly property string _configDir: _configUrl.startsWith(
|
||||
"file://") ? _configUrl.substring(
|
||||
7) : _configUrl
|
||||
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||
readonly property string _configDir: Paths.strip(_configUrl)
|
||||
|
||||
signal forceTopBarLayoutRefresh
|
||||
signal widgetDataChanged
|
||||
signal workspaceIconsUpdated
|
||||
|
||||
|
||||
property bool _loading: false
|
||||
|
||||
function getEffectiveTimeFormat() {
|
||||
if (use24HourClock) {
|
||||
return Locale.ShortFormat
|
||||
@@ -115,11 +155,11 @@ Singleton {
|
||||
return "h:mm AP"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getEffectiveClockDateFormat() {
|
||||
return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d"
|
||||
}
|
||||
|
||||
|
||||
function getEffectiveLockDateFormat() {
|
||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat
|
||||
}
|
||||
@@ -132,22 +172,26 @@ Singleton {
|
||||
"enabled": true,
|
||||
"size": 20,
|
||||
"selectedGpuIndex": 0,
|
||||
"pciId": ""
|
||||
"pciId": "",
|
||||
"mountPath": "/"
|
||||
}
|
||||
leftWidgetsModel.append(dummyItem)
|
||||
centerWidgetsModel.append(dummyItem)
|
||||
rightWidgetsModel.append(dummyItem)
|
||||
|
||||
|
||||
updateListModel(leftWidgetsModel, topBarLeftWidgets)
|
||||
updateListModel(centerWidgetsModel, topBarCenterWidgets)
|
||||
updateListModel(rightWidgetsModel, topBarRightWidgets)
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
_loading = true
|
||||
parseSettings(settingsFile.text())
|
||||
_loading = false
|
||||
}
|
||||
|
||||
function parseSettings(content) {
|
||||
_loading = true
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
var settings = JSON.parse(content)
|
||||
@@ -164,42 +208,21 @@ Singleton {
|
||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue"
|
||||
}
|
||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""
|
||||
topBarTransparency = settings.topBarTransparency
|
||||
!== undefined ? (settings.topBarTransparency
|
||||
> 1 ? settings.topBarTransparency
|
||||
/ 100 : settings.topBarTransparency) : 0.75
|
||||
topBarWidgetTransparency = settings.topBarWidgetTransparency
|
||||
!== undefined ? (settings.topBarWidgetTransparency
|
||||
> 1 ? settings.topBarWidgetTransparency
|
||||
/ 100 : settings.topBarWidgetTransparency) : 0.85
|
||||
popupTransparency = settings.popupTransparency
|
||||
!== undefined ? (settings.popupTransparency
|
||||
> 1 ? settings.popupTransparency
|
||||
/ 100 : settings.popupTransparency) : 0.92
|
||||
dockTransparency = settings.dockTransparency
|
||||
!== undefined ? (settings.dockTransparency
|
||||
> 1 ? settings.dockTransparency
|
||||
/ 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
|
||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"
|
||||
topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 0.75
|
||||
topBarWidgetTransparency = settings.topBarWidgetTransparency !== undefined ? (settings.topBarWidgetTransparency > 1 ? settings.topBarWidgetTransparency / 100 : settings.topBarWidgetTransparency) : 0.85
|
||||
popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 1.0
|
||||
dockTransparency = settings.dockTransparency !== undefined ? (settings.dockTransparency > 1 ? settings.dockTransparency / 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
|
||||
showMusic = settings.showMusic !== undefined ? settings.showMusic : true
|
||||
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true
|
||||
@@ -207,64 +230,54 @@ Singleton {
|
||||
showMemUsage = settings.showMemUsage !== undefined ? settings.showMemUsage : true
|
||||
showCpuTemp = settings.showCpuTemp !== undefined ? settings.showCpuTemp : true
|
||||
showGpuTemp = settings.showGpuTemp !== undefined ? settings.showGpuTemp : true
|
||||
selectedGpuIndex = settings.selectedGpuIndex
|
||||
!== undefined ? settings.selectedGpuIndex : 0
|
||||
enabledGpuPciIds = settings.enabledGpuPciIds
|
||||
!== undefined ? settings.enabledGpuPciIds : []
|
||||
showSystemTray = settings.showSystemTray
|
||||
!== undefined ? settings.showSystemTray : true
|
||||
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
||||
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
|
||||
showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true
|
||||
showClock = settings.showClock !== undefined ? settings.showClock : true
|
||||
showNotificationButton = settings.showNotificationButton
|
||||
!== undefined ? settings.showNotificationButton : true
|
||||
showNotificationButton = settings.showNotificationButton !== undefined ? settings.showNotificationButton : true
|
||||
showBattery = settings.showBattery !== undefined ? settings.showBattery : true
|
||||
showControlCenterButton = settings.showControlCenterButton
|
||||
!== undefined ? settings.showControlCenterButton : true
|
||||
controlCenterShowNetworkIcon = settings.controlCenterShowNetworkIcon
|
||||
!== undefined ? settings.controlCenterShowNetworkIcon : true
|
||||
controlCenterShowBluetoothIcon = settings.controlCenterShowBluetoothIcon
|
||||
!== undefined ? settings.controlCenterShowBluetoothIcon : true
|
||||
controlCenterShowAudioIcon = settings.controlCenterShowAudioIcon
|
||||
!== undefined ? settings.controlCenterShowAudioIcon : true
|
||||
showWorkspaceIndex = settings.showWorkspaceIndex
|
||||
!== undefined ? settings.showWorkspaceIndex : false
|
||||
showWorkspacePadding = settings.showWorkspacePadding
|
||||
!== undefined ? settings.showWorkspacePadding : false
|
||||
workspaceNameIcons = settings.workspaceNameIcons
|
||||
!== undefined ? settings.workspaceNameIcons : ({})
|
||||
clockCompactMode = settings.clockCompactMode
|
||||
!== undefined ? settings.clockCompactMode : false
|
||||
focusedWindowCompactMode = settings.focusedWindowCompactMode
|
||||
!== undefined ? settings.focusedWindowCompactMode : false
|
||||
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 : ""
|
||||
showControlCenterButton = settings.showControlCenterButton !== undefined ? settings.showControlCenterButton : true
|
||||
controlCenterShowNetworkIcon = settings.controlCenterShowNetworkIcon !== undefined ? settings.controlCenterShowNetworkIcon : true
|
||||
controlCenterShowBluetoothIcon = settings.controlCenterShowBluetoothIcon !== undefined ? settings.controlCenterShowBluetoothIcon : true
|
||||
controlCenterShowAudioIcon = settings.controlCenterShowAudioIcon !== undefined ? settings.controlCenterShowAudioIcon : true
|
||||
controlCenterWidgets = settings.controlCenterWidgets !== undefined ? settings.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}
|
||||
]
|
||||
showWorkspaceIndex = settings.showWorkspaceIndex !== undefined ? settings.showWorkspaceIndex : false
|
||||
showWorkspacePadding = settings.showWorkspacePadding !== undefined ? settings.showWorkspacePadding : false
|
||||
showWorkspaceApps = settings.showWorkspaceApps !== undefined ? settings.showWorkspaceApps : false
|
||||
maxWorkspaceIcons = settings.maxWorkspaceIcons !== undefined ? settings.maxWorkspaceIcons : 3
|
||||
workspaceNameIcons = settings.workspaceNameIcons !== undefined ? settings.workspaceNameIcons : ({})
|
||||
workspacesPerMonitor = settings.workspacesPerMonitor !== undefined ? settings.workspacesPerMonitor : true
|
||||
waveProgressEnabled = settings.waveProgressEnabled !== undefined ? settings.waveProgressEnabled : true
|
||||
clockCompactMode = settings.clockCompactMode !== undefined ? settings.clockCompactMode : false
|
||||
focusedWindowCompactMode = settings.focusedWindowCompactMode !== undefined ? settings.focusedWindowCompactMode : false
|
||||
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)
|
||||
if (settings.topBarWidgetOrder) {
|
||||
topBarLeftWidgets = settings.topBarWidgetOrder.filter(w => {
|
||||
return ["launcherButton", "workspaceSwitcher", "focusedWindow"].includes(w)
|
||||
})
|
||||
topBarCenterWidgets = settings.topBarWidgetOrder.filter(
|
||||
w => {
|
||||
return ["clock", "music", "weather"].includes(
|
||||
w)
|
||||
})
|
||||
topBarRightWidgets = settings.topBarWidgetOrder.filter(
|
||||
w => {
|
||||
return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].includes(
|
||||
w)
|
||||
})
|
||||
topBarCenterWidgets = settings.topBarWidgetOrder.filter(w => {
|
||||
return ["clock", "music", "weather"].includes(w)
|
||||
})
|
||||
topBarRightWidgets = settings.topBarWidgetOrder.filter(w => {
|
||||
return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].includes(w)
|
||||
})
|
||||
} else {
|
||||
var leftWidgets = settings.topBarLeftWidgets
|
||||
!== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
||||
var centerWidgets = settings.topBarCenterWidgets
|
||||
!== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"]
|
||||
var rightWidgets = settings.topBarRightWidgets
|
||||
!== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
|
||||
var leftWidgets = settings.topBarLeftWidgets !== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
||||
var centerWidgets = settings.topBarCenterWidgets !== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"]
|
||||
var rightWidgets = settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
|
||||
topBarLeftWidgets = leftWidgets
|
||||
topBarCenterWidgets = centerWidgets
|
||||
topBarRightWidgets = rightWidgets
|
||||
@@ -272,54 +285,48 @@ Singleton {
|
||||
updateListModel(centerWidgetsModel, centerWidgets)
|
||||
updateListModel(rightWidgetsModel, rightWidgets)
|
||||
}
|
||||
appLauncherViewMode = settings.appLauncherViewMode
|
||||
!== undefined ? settings.appLauncherViewMode : "list"
|
||||
spotlightModalViewMode = settings.spotlightModalViewMode
|
||||
!== undefined ? settings.spotlightModalViewMode : "list"
|
||||
networkPreference = settings.networkPreference
|
||||
!== undefined ? settings.networkPreference : "auto"
|
||||
appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list"
|
||||
spotlightModalViewMode = settings.spotlightModalViewMode !== undefined ? settings.spotlightModalViewMode : "list"
|
||||
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto"
|
||||
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default"
|
||||
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false
|
||||
osLogoColorOverride = settings.osLogoColorOverride
|
||||
!== undefined ? settings.osLogoColorOverride : ""
|
||||
osLogoBrightness = settings.osLogoBrightness
|
||||
!== undefined ? settings.osLogoBrightness : 0.5
|
||||
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : ""
|
||||
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5
|
||||
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1
|
||||
wallpaperDynamicTheming = settings.wallpaperDynamicTheming
|
||||
!== undefined ? settings.wallpaperDynamicTheming : true
|
||||
fontFamily = settings.fontFamily
|
||||
!== undefined ? settings.fontFamily : defaultFontFamily
|
||||
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
|
||||
gtkThemingEnabled = settings.gtkThemingEnabled
|
||||
!== undefined ? settings.gtkThemingEnabled : false
|
||||
qtThemingEnabled = settings.qtThemingEnabled
|
||||
!== undefined ? settings.qtThemingEnabled : false
|
||||
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0
|
||||
notepadUseMonospace = settings.notepadUseMonospace !== undefined ? settings.notepadUseMonospace : true
|
||||
notepadFontFamily = settings.notepadFontFamily !== undefined ? settings.notepadFontFamily : ""
|
||||
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
|
||||
dockAutoHide = settings.dockAutoHide !== undefined ? settings.dockAutoHide : false
|
||||
dockGroupByApp = settings.dockGroupByApp !== undefined ? settings.dockGroupByApp : false
|
||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12
|
||||
notificationOverlayEnabled = settings.notificationOverlayEnabled
|
||||
!== undefined ? settings.notificationOverlayEnabled : false
|
||||
topBarAutoHide = settings.topBarAutoHide
|
||||
!== undefined ? settings.topBarAutoHide : false
|
||||
topBarVisible = settings.topBarVisible
|
||||
!== undefined ? settings.topBarVisible : true
|
||||
notificationTimeoutLow = settings.notificationTimeoutLow
|
||||
!== undefined ? settings.notificationTimeoutLow : 5000
|
||||
notificationTimeoutNormal = settings.notificationTimeoutNormal
|
||||
!== undefined ? settings.notificationTimeoutNormal : 5000
|
||||
notificationTimeoutCritical = settings.notificationTimeoutCritical
|
||||
!== undefined ? settings.notificationTimeoutCritical : 0
|
||||
notificationOverlayEnabled = settings.notificationOverlayEnabled !== undefined ? settings.notificationOverlayEnabled : false
|
||||
topBarAutoHide = settings.topBarAutoHide !== undefined ? settings.topBarAutoHide : false
|
||||
topBarOpenOnOverview = settings.topBarOpenOnOverview !== undefined ? settings.topBarOpenOnOverview : false
|
||||
topBarVisible = settings.topBarVisible !== undefined ? settings.topBarVisible : true
|
||||
notificationTimeoutLow = settings.notificationTimeoutLow !== undefined ? settings.notificationTimeoutLow : 5000
|
||||
notificationTimeoutNormal = settings.notificationTimeoutNormal !== undefined ? settings.notificationTimeoutNormal : 5000
|
||||
notificationTimeoutCritical = settings.notificationTimeoutCritical !== undefined ? settings.notificationTimeoutCritical : 0
|
||||
topBarSpacing = settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4
|
||||
topBarBottomGap = settings.topBarBottomGap !== undefined ? settings.topBarBottomGap : 0
|
||||
topBarInnerPadding = settings.topBarInnerPadding !== undefined ? settings.topBarInnerPadding : 8
|
||||
topBarSquareCorners = settings.topBarSquareCorners
|
||||
!== undefined ? settings.topBarSquareCorners : false
|
||||
topBarNoBackground = settings.topBarNoBackground
|
||||
!== undefined ? settings.topBarNoBackground : false
|
||||
screenPreferences = settings.screenPreferences
|
||||
!== undefined ? settings.screenPreferences : ({})
|
||||
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 : "s"
|
||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
|
||||
applyStoredTheme()
|
||||
detectAvailableIconThemes()
|
||||
detectQtTools()
|
||||
@@ -330,13 +337,18 @@ Singleton {
|
||||
}
|
||||
} catch (e) {
|
||||
applyStoredTheme()
|
||||
} finally {
|
||||
_loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (_loading)
|
||||
return
|
||||
settingsFile.setText(JSON.stringify({
|
||||
"currentThemeName": currentThemeName,
|
||||
"customThemeFile": customThemeFile,
|
||||
"matugenScheme": matugenScheme,
|
||||
"topBarTransparency": topBarTransparency,
|
||||
"topBarWidgetTransparency": topBarWidgetTransparency,
|
||||
"popupTransparency": popupTransparency,
|
||||
@@ -368,9 +380,14 @@ Singleton {
|
||||
"controlCenterShowNetworkIcon": controlCenterShowNetworkIcon,
|
||||
"controlCenterShowBluetoothIcon": controlCenterShowBluetoothIcon,
|
||||
"controlCenterShowAudioIcon": controlCenterShowAudioIcon,
|
||||
"controlCenterWidgets": controlCenterWidgets,
|
||||
"showWorkspaceIndex": showWorkspaceIndex,
|
||||
"showWorkspacePadding": showWorkspacePadding,
|
||||
"showWorkspaceApps": showWorkspaceApps,
|
||||
"maxWorkspaceIcons": maxWorkspaceIcons,
|
||||
"workspacesPerMonitor": workspacesPerMonitor,
|
||||
"workspaceNameIcons": workspaceNameIcons,
|
||||
"waveProgressEnabled": waveProgressEnabled,
|
||||
"clockCompactMode": clockCompactMode,
|
||||
"focusedWindowCompactMode": focusedWindowCompactMode,
|
||||
"runningAppsCompactMode": runningAppsCompactMode,
|
||||
@@ -389,23 +406,36 @@ Singleton {
|
||||
"osLogoColorOverride": osLogoColorOverride,
|
||||
"osLogoBrightness": osLogoBrightness,
|
||||
"osLogoContrast": osLogoContrast,
|
||||
"wallpaperDynamicTheming": wallpaperDynamicTheming,
|
||||
"fontFamily": fontFamily,
|
||||
"monoFontFamily": monoFontFamily,
|
||||
"fontWeight": fontWeight,
|
||||
"fontScale": fontScale,
|
||||
"notepadUseMonospace": notepadUseMonospace,
|
||||
"notepadFontFamily": notepadFontFamily,
|
||||
"notepadFontSize": notepadFontSize,
|
||||
"notepadShowLineNumbers": notepadShowLineNumbers,
|
||||
"notepadTransparencyOverride": notepadTransparencyOverride,
|
||||
"notepadLastCustomTransparency": notepadLastCustomTransparency,
|
||||
"gtkThemingEnabled": gtkThemingEnabled,
|
||||
"qtThemingEnabled": qtThemingEnabled,
|
||||
"showDock": showDock,
|
||||
"dockAutoHide": dockAutoHide,
|
||||
"dockGroupByApp": dockGroupByApp,
|
||||
"cornerRadius": cornerRadius,
|
||||
"notificationOverlayEnabled": notificationOverlayEnabled,
|
||||
"topBarAutoHide": topBarAutoHide,
|
||||
"topBarOpenOnOverview": topBarOpenOnOverview,
|
||||
"topBarVisible": topBarVisible,
|
||||
"topBarSpacing": topBarSpacing,
|
||||
"topBarBottomGap": topBarBottomGap,
|
||||
"topBarInnerPadding": topBarInnerPadding,
|
||||
"topBarSquareCorners": topBarSquareCorners,
|
||||
"topBarNoBackground": topBarNoBackground,
|
||||
"topBarGothCornersEnabled": topBarGothCornersEnabled,
|
||||
"lockScreenShowPowerActions": lockScreenShowPowerActions,
|
||||
"hideBrightnessSlider": hideBrightnessSlider,
|
||||
"widgetBackgroundColor": widgetBackgroundColor,
|
||||
"surfaceBase": surfaceBase,
|
||||
"notificationTimeoutLow": notificationTimeoutLow,
|
||||
"notificationTimeoutNormal": notificationTimeoutNormal,
|
||||
"notificationTimeoutCritical": notificationTimeoutCritical,
|
||||
@@ -423,6 +453,26 @@ Singleton {
|
||||
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) {
|
||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
||||
iconMap[workspaceName] = iconData
|
||||
@@ -460,8 +510,7 @@ Singleton {
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
|
||||
return namedWorkspaces
|
||||
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
var ws = NiriService.allWorkspaces[i]
|
||||
for (const ws of NiriService.allWorkspaces) {
|
||||
if (ws.name && ws.name.trim() !== "") {
|
||||
namedWorkspaces.push(ws.name)
|
||||
}
|
||||
@@ -510,7 +559,7 @@ Singleton {
|
||||
else
|
||||
Qt.callLater(() => {
|
||||
if (typeof Theme !== "undefined")
|
||||
Theme.switchTheme(currentThemeName, false)
|
||||
Theme.switchTheme(currentThemeName, false)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -524,6 +573,19 @@ Singleton {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setMatugenScheme(scheme) {
|
||||
var normalized = scheme || "scheme-tonal-spot"
|
||||
if (matugenScheme === normalized)
|
||||
return
|
||||
|
||||
matugenScheme = normalized
|
||||
saveSettings()
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setTopBarTransparency(transparency) {
|
||||
topBarTransparency = transparency
|
||||
saveSettings()
|
||||
@@ -660,6 +722,10 @@ Singleton {
|
||||
controlCenterShowAudioIcon = enabled
|
||||
saveSettings()
|
||||
}
|
||||
function setControlCenterWidgets(widgets) {
|
||||
controlCenterWidgets = widgets
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setTopBarWidgetOrder(order) {
|
||||
topBarWidgetOrder = order
|
||||
@@ -690,9 +756,9 @@ Singleton {
|
||||
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id
|
||||
var enabled = typeof order[i] === "string" ? true : order[i].enabled
|
||||
var size = typeof order[i] === "string" ? undefined : order[i].size
|
||||
var selectedGpuIndex = typeof order[i]
|
||||
=== "string" ? undefined : order[i].selectedGpuIndex
|
||||
var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex
|
||||
var pciId = typeof order[i] === "string" ? undefined : order[i].pciId
|
||||
var mountPath = typeof order[i] === "string" ? undefined : order[i].mountPath
|
||||
var item = {
|
||||
"widgetId": widgetId,
|
||||
"enabled": enabled
|
||||
@@ -703,6 +769,8 @@ Singleton {
|
||||
item.selectedGpuIndex = selectedGpuIndex
|
||||
if (pciId !== undefined)
|
||||
item.pciId = pciId
|
||||
if (mountPath !== undefined)
|
||||
item.mountPath = mountPath
|
||||
|
||||
listModel.append(item)
|
||||
}
|
||||
@@ -807,19 +875,16 @@ Singleton {
|
||||
|
||||
function updateQtIconTheme(themeName) {
|
||||
var qtThemeName = (themeName === "System Default") ? "" : themeName
|
||||
var home = _shq(root._homeUrl.replace("file://", ""))
|
||||
var home = _shq(Paths.strip(root._homeUrl))
|
||||
if (!qtThemeName) {
|
||||
// When "System Default" is selected, don't modify the config files at all
|
||||
// This preserves the user's existing qt6ct configuration
|
||||
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"
|
||||
+ " 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"
|
||||
+ " 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"
|
||||
+ " 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"
|
||||
+ " 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"
|
||||
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" + " 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"
|
||||
+ " 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(
|
||||
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"
|
||||
Quickshell.execDetached(["sh", "-lc", script])
|
||||
}
|
||||
|
||||
@@ -848,11 +913,6 @@ Singleton {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperDynamicTheming(enabled) {
|
||||
wallpaperDynamicTheming = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setFontFamily(family) {
|
||||
fontFamily = family
|
||||
saveSettings()
|
||||
@@ -868,6 +928,11 @@ Singleton {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setFontScale(scale) {
|
||||
fontScale = scale
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setGtkThemingEnabled(enabled) {
|
||||
gtkThemingEnabled = enabled
|
||||
saveSettings()
|
||||
@@ -894,6 +959,11 @@ Singleton {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDockGroupByApp(enabled) {
|
||||
dockGroupByApp = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setCornerRadius(radius) {
|
||||
cornerRadius = radius
|
||||
saveSettings()
|
||||
@@ -909,6 +979,11 @@ Singleton {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setTopBarOpenOnOverview(enabled) {
|
||||
topBarOpenOnOverview = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setTopBarVisible(visible) {
|
||||
topBarVisible = visible
|
||||
saveSettings()
|
||||
@@ -959,6 +1034,34 @@ Singleton {
|
||||
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()
|
||||
@@ -1002,17 +1105,14 @@ Singleton {
|
||||
onTriggered: {
|
||||
var availableFonts = Qt.fontFamilies()
|
||||
var missingFonts = []
|
||||
if (fontFamily === defaultFontFamily && !availableFonts.includes(
|
||||
defaultFontFamily))
|
||||
missingFonts.push(defaultFontFamily)
|
||||
if (fontFamily === defaultFontFamily && !availableFonts.includes(defaultFontFamily))
|
||||
missingFonts.push(defaultFontFamily)
|
||||
|
||||
if (monoFontFamily === defaultMonoFontFamily
|
||||
&& !availableFonts.includes(defaultMonoFontFamily))
|
||||
missingFonts.push(defaultMonoFontFamily)
|
||||
if (monoFontFamily === defaultMonoFontFamily && !availableFonts.includes(defaultMonoFontFamily))
|
||||
missingFonts.push(defaultMonoFontFamily)
|
||||
|
||||
if (missingFonts.length > 0) {
|
||||
var message = "Missing fonts: " + missingFonts.join(
|
||||
", ") + ". Using system defaults."
|
||||
var message = "Missing fonts: " + missingFonts.join(", ") + ". Using system defaults."
|
||||
ToastService.showWarning(message)
|
||||
}
|
||||
}
|
||||
@@ -1023,23 +1123,23 @@ Singleton {
|
||||
FileView {
|
||||
id: settingsFile
|
||||
|
||||
path: StandardPaths.writableLocation(
|
||||
StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
watchChanges: true
|
||||
onLoaded: {
|
||||
parseSettings(settingsFile.text())
|
||||
hasTriedDefaultSettings = false
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!hasTriedDefaultSettings) {
|
||||
hasTriedDefaultSettings = true
|
||||
defaultSettingsCheckProcess.running = true
|
||||
} else {
|
||||
applyStoredTheme()
|
||||
}
|
||||
}
|
||||
if (!hasTriedDefaultSettings) {
|
||||
hasTriedDefaultSettings = true
|
||||
defaultSettingsCheckProcess.running = true
|
||||
} else {
|
||||
applyStoredTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -1048,12 +1148,12 @@ Singleton {
|
||||
command: ["sh", "-c", "gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed \"s/'//g\" || echo ''"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0 && stdout && stdout.length > 0)
|
||||
systemDefaultIconTheme = stdout.trim()
|
||||
else
|
||||
systemDefaultIconTheme = ""
|
||||
iconThemeDetectionProcess.running = true
|
||||
}
|
||||
if (exitCode === 0 && stdout && stdout.length > 0)
|
||||
systemDefaultIconTheme = stdout.trim()
|
||||
else
|
||||
systemDefaultIconTheme = ""
|
||||
iconThemeDetectionProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -1069,9 +1169,8 @@ Singleton {
|
||||
var themes = text.trim().split('\n')
|
||||
for (var i = 0; i < themes.length; i++) {
|
||||
var theme = themes[i].trim()
|
||||
if (theme && theme !== "" && theme !== "default"
|
||||
&& theme !== "hicolor" && theme !== "locolor")
|
||||
detectedThemes.push(theme)
|
||||
if (theme && theme !== "" && theme !== "default" && theme !== "hicolor" && theme !== "locolor")
|
||||
detectedThemes.push(theme)
|
||||
}
|
||||
}
|
||||
availableIconThemes = detectedThemes
|
||||
@@ -1092,11 +1191,11 @@ Singleton {
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i]
|
||||
if (line.startsWith('qt5ct:'))
|
||||
qt5ctAvailable = line.split(':')[1] === 'true'
|
||||
qt5ctAvailable = line.split(':')[1] === 'true'
|
||||
else if (line.startsWith('qt6ct:'))
|
||||
qt6ctAvailable = line.split(':')[1] === 'true'
|
||||
qt6ctAvailable = line.split(':')[1] === 'true'
|
||||
else if (line.startsWith('gtk:'))
|
||||
gtkAvailable = line.split(':')[1] === 'true'
|
||||
gtkAvailable = line.split(':')[1] === 'true'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1106,40 +1205,40 @@ Singleton {
|
||||
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"]
|
||||
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()
|
||||
}
|
||||
}
|
||||
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 show() {
|
||||
function reveal(): string {
|
||||
root.setTopBarVisible(true)
|
||||
return "BAR_SHOW_SUCCESS"
|
||||
}
|
||||
|
||||
function hide() {
|
||||
function hide(): string {
|
||||
root.setTopBarVisible(false)
|
||||
return "BAR_HIDE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
function toggle(): string {
|
||||
root.toggleTopBarVisible()
|
||||
return topBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS"
|
||||
}
|
||||
|
||||
function status() {
|
||||
function status(): string {
|
||||
return topBarVisible ? "visible" : "hidden"
|
||||
}
|
||||
|
||||
target: "bar"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,41 +1,136 @@
|
||||
// Stock theme definitions for DankMaterialShell
|
||||
// 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 = {
|
||||
DARK: {
|
||||
blue: {
|
||||
name: "Blue",
|
||||
primary: "#42a5f5",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#1976d2",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#0d47a1",
|
||||
secondary: "#8ab4f8",
|
||||
surface: "#1a1c1e",
|
||||
surfaceText: "#e3e8ef",
|
||||
surfaceVariant: "#44464f",
|
||||
surfaceVariantText: "#c4c7c5",
|
||||
surface: "#101418",
|
||||
surfaceText: "#e0e2e8",
|
||||
surfaceVariant: "#42474e",
|
||||
surfaceVariantText: "#c2c7cf",
|
||||
surfaceTint: "#8ab4f8",
|
||||
background: "#1a1c1e",
|
||||
backgroundText: "#e3e8ef",
|
||||
outline: "#8e918f",
|
||||
surfaceContainer: "#1e2023",
|
||||
surfaceContainerHigh: "#292b2f"
|
||||
},
|
||||
deepBlue: {
|
||||
name: "Deep Blue",
|
||||
primary: "#0061a4",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#004881",
|
||||
secondary: "#42a5f5",
|
||||
surface: "#1a1c1e",
|
||||
surfaceText: "#e3e8ef",
|
||||
surfaceVariant: "#44464f",
|
||||
surfaceVariantText: "#c4c7c5",
|
||||
surfaceTint: "#8ab4f8",
|
||||
background: "#1a1c1e",
|
||||
backgroundText: "#e3e8ef",
|
||||
outline: "#8e918f",
|
||||
surfaceContainer: "#1e2023",
|
||||
surfaceContainerHigh: "#292b2f"
|
||||
background: "#101418",
|
||||
backgroundText: "#e0e2e8",
|
||||
outline: "#8c9199",
|
||||
surfaceContainer: "#1d2024",
|
||||
surfaceContainerHigh: "#272a2f",
|
||||
surfaceContainerHighest: "#32353a"
|
||||
},
|
||||
purple: {
|
||||
name: "Purple",
|
||||
@@ -43,135 +138,165 @@ const StockThemes = {
|
||||
primaryText: "#381E72",
|
||||
primaryContainer: "#4F378B",
|
||||
secondary: "#CCC2DC",
|
||||
surface: "#10121E",
|
||||
surfaceText: "#E6E0E9",
|
||||
surfaceVariant: "#49454F",
|
||||
surfaceVariantText: "#CAC4D0",
|
||||
surface: "#141218",
|
||||
surfaceText: "#e6e0e9",
|
||||
surfaceVariant: "#49454e",
|
||||
surfaceVariantText: "#cac4cf",
|
||||
surfaceTint: "#D0BCFF",
|
||||
background: "#10121E",
|
||||
backgroundText: "#E6E0E9",
|
||||
outline: "#938F99",
|
||||
surfaceContainer: "#1D1B20",
|
||||
surfaceContainerHigh: "#2B2930"
|
||||
background: "#141218",
|
||||
backgroundText: "#e6e0e9",
|
||||
outline: "#948f99",
|
||||
surfaceContainer: "#211f24",
|
||||
surfaceContainerHigh: "#2b292f",
|
||||
surfaceContainerHighest: "#36343a"
|
||||
},
|
||||
green: {
|
||||
name: "Green",
|
||||
primary: "#4caf50",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#388e3c",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#1b5e20",
|
||||
secondary: "#81c995",
|
||||
surface: "#0f1411",
|
||||
surfaceText: "#e1f5e3",
|
||||
surfaceVariant: "#404943",
|
||||
surfaceVariantText: "#c1cbc4",
|
||||
surface: "#10140f",
|
||||
surfaceText: "#e0e4db",
|
||||
surfaceVariant: "#424940",
|
||||
surfaceVariantText: "#c2c9bd",
|
||||
surfaceTint: "#81c995",
|
||||
background: "#0f1411",
|
||||
backgroundText: "#e1f5e3",
|
||||
outline: "#8b938c",
|
||||
surfaceContainer: "#1a1f1b",
|
||||
surfaceContainerHigh: "#252a26"
|
||||
background: "#10140f",
|
||||
backgroundText: "#e0e4db",
|
||||
outline: "#8c9388",
|
||||
surfaceContainer: "#1d211b",
|
||||
surfaceContainerHigh: "#272b25",
|
||||
surfaceContainerHighest: "#323630"
|
||||
},
|
||||
orange: {
|
||||
name: "Orange",
|
||||
primary: "#ff6d00",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#e65100",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#3e2723",
|
||||
secondary: "#ffb74d",
|
||||
surface: "#1c1410",
|
||||
surfaceText: "#f5f1ea",
|
||||
surfaceVariant: "#4a453a",
|
||||
surfaceVariantText: "#cbc5b8",
|
||||
surface: "#1a120e",
|
||||
surfaceText: "#f0dfd8",
|
||||
surfaceVariant: "#52443d",
|
||||
surfaceVariantText: "#d7c2b9",
|
||||
surfaceTint: "#ffb74d",
|
||||
background: "#1c1410",
|
||||
backgroundText: "#f5f1ea",
|
||||
outline: "#958f84",
|
||||
surfaceContainer: "#211e17",
|
||||
surfaceContainerHigh: "#2c291f"
|
||||
background: "#1a120e",
|
||||
backgroundText: "#f0dfd8",
|
||||
outline: "#a08d85",
|
||||
surfaceContainer: "#271e1a",
|
||||
surfaceContainerHigh: "#322824",
|
||||
surfaceContainerHighest: "#3d332e"
|
||||
},
|
||||
red: {
|
||||
name: "Red",
|
||||
primary: "#f44336",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#d32f2f",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#4a0e0e",
|
||||
secondary: "#f28b82",
|
||||
surface: "#1c1011",
|
||||
surfaceText: "#f5e8ea",
|
||||
surfaceVariant: "#4a3f41",
|
||||
surfaceVariantText: "#cbc2c4",
|
||||
surface: "#1a1110",
|
||||
surfaceText: "#f1dedc",
|
||||
surfaceVariant: "#534341",
|
||||
surfaceVariantText: "#d8c2be",
|
||||
surfaceTint: "#f28b82",
|
||||
background: "#1c1011",
|
||||
backgroundText: "#f5e8ea",
|
||||
outline: "#958b8d",
|
||||
surfaceContainer: "#211b1c",
|
||||
surfaceContainerHigh: "#2c2426"
|
||||
background: "#1a1110",
|
||||
backgroundText: "#f1dedc",
|
||||
outline: "#a08c89",
|
||||
surfaceContainer: "#271d1c",
|
||||
surfaceContainerHigh: "#322826",
|
||||
surfaceContainerHighest: "#3d3231"
|
||||
},
|
||||
cyan: {
|
||||
name: "Cyan",
|
||||
primary: "#00bcd4",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#0097a7",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#004d5c",
|
||||
secondary: "#4dd0e1",
|
||||
surface: "#0f1617",
|
||||
surfaceText: "#e8f4f5",
|
||||
surfaceVariant: "#3f474a",
|
||||
surfaceVariantText: "#c2c9cb",
|
||||
surface: "#0e1416",
|
||||
surfaceText: "#dee3e5",
|
||||
surfaceVariant: "#3f484a",
|
||||
surfaceVariantText: "#bfc8ca",
|
||||
surfaceTint: "#4dd0e1",
|
||||
background: "#0f1617",
|
||||
backgroundText: "#e8f4f5",
|
||||
outline: "#8c9194",
|
||||
surfaceContainer: "#1a1f20",
|
||||
surfaceContainerHigh: "#252b2c"
|
||||
background: "#0e1416",
|
||||
backgroundText: "#dee3e5",
|
||||
outline: "#899295",
|
||||
surfaceContainer: "#1b2122",
|
||||
surfaceContainerHigh: "#252b2c",
|
||||
surfaceContainerHighest: "#303637"
|
||||
},
|
||||
pink: {
|
||||
name: "Pink",
|
||||
primary: "#e91e63",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#c2185b",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#4a0e2f",
|
||||
secondary: "#f8bbd9",
|
||||
surface: "#1a1014",
|
||||
surfaceText: "#f3e8ee",
|
||||
surfaceVariant: "#483f45",
|
||||
surfaceVariantText: "#c9c2c7",
|
||||
surface: "#191112",
|
||||
surfaceText: "#f0dee0",
|
||||
surfaceVariant: "#524345",
|
||||
surfaceVariantText: "#d6c2c3",
|
||||
surfaceTint: "#f8bbd9",
|
||||
background: "#1a1014",
|
||||
backgroundText: "#f3e8ee",
|
||||
outline: "#938a90",
|
||||
surfaceContainer: "#1f1b1e",
|
||||
surfaceContainerHigh: "#2a2428"
|
||||
background: "#191112",
|
||||
backgroundText: "#f0dee0",
|
||||
outline: "#9f8c8e",
|
||||
surfaceContainer: "#261d1e",
|
||||
surfaceContainerHigh: "#312829",
|
||||
surfaceContainerHighest: "#3c3233"
|
||||
},
|
||||
amber: {
|
||||
name: "Amber",
|
||||
primary: "#ffc107",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#ff8f00",
|
||||
primaryContainer: "#4a3c00",
|
||||
secondary: "#ffd54f",
|
||||
surface: "#1a1710",
|
||||
surfaceText: "#f3f0e8",
|
||||
surfaceVariant: "#49453a",
|
||||
surfaceVariantText: "#cac5b8",
|
||||
surface: "#17130b",
|
||||
surfaceText: "#ebe1d4",
|
||||
surfaceVariant: "#4d4639",
|
||||
surfaceVariantText: "#d0c5b4",
|
||||
surfaceTint: "#ffd54f",
|
||||
background: "#1a1710",
|
||||
backgroundText: "#f3f0e8",
|
||||
outline: "#949084",
|
||||
surfaceContainer: "#1f1e17",
|
||||
surfaceContainerHigh: "#2a281f"
|
||||
background: "#17130b",
|
||||
backgroundText: "#ebe1d4",
|
||||
outline: "#998f80",
|
||||
surfaceContainer: "#231f17",
|
||||
surfaceContainerHigh: "#2e2921",
|
||||
surfaceContainerHighest: "#39342b"
|
||||
},
|
||||
coral: {
|
||||
name: "Coral",
|
||||
primary: "#ffb4ab",
|
||||
primaryText: "#5f1412",
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#8c1d18",
|
||||
secondary: "#f9dedc",
|
||||
surface: "#1a1110",
|
||||
surfaceText: "#f1e8e7",
|
||||
surfaceVariant: "#4a4142",
|
||||
surfaceVariantText: "#cdc2c1",
|
||||
surfaceText: "#f1dedc",
|
||||
surfaceVariant: "#534341",
|
||||
surfaceVariantText: "#d8c2bf",
|
||||
surfaceTint: "#ffb4ab",
|
||||
background: "#1a1110",
|
||||
backgroundText: "#f1e8e7",
|
||||
outline: "#968b8a",
|
||||
surfaceContainer: "#201a19",
|
||||
surfaceContainerHigh: "#2b2221"
|
||||
backgroundText: "#f1dedc",
|
||||
outline: "#a08c8a",
|
||||
surfaceContainer: "#271d1c",
|
||||
surfaceContainerHigh: "#322826",
|
||||
surfaceContainerHighest: "#3d3231"
|
||||
},
|
||||
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: {
|
||||
@@ -181,33 +306,17 @@ const StockThemes = {
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#e3f2fd",
|
||||
secondary: "#42a5f5",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surface: "#f7f9ff",
|
||||
surfaceText: "#181c20",
|
||||
surfaceVariant: "#dee3eb",
|
||||
surfaceVariantText: "#42474e",
|
||||
surfaceTint: "#1976d2",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
},
|
||||
deepBlue: {
|
||||
name: "Deep Blue Light",
|
||||
primary: "#0061a4",
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#cfe5ff",
|
||||
secondary: "#1976d2",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surfaceTint: "#0061a4",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
background: "#f7f9ff",
|
||||
backgroundText: "#181c20",
|
||||
outline: "#72777f",
|
||||
surfaceContainer: "#eceef4",
|
||||
surfaceContainerHigh: "#e6e8ee",
|
||||
surfaceContainerHighest: "#e0e2e8"
|
||||
},
|
||||
purple: {
|
||||
name: "Purple Light",
|
||||
@@ -215,16 +324,17 @@ const StockThemes = {
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#EADDFF",
|
||||
secondary: "#625B71",
|
||||
surface: "#FFFBFE",
|
||||
surfaceText: "#1C1B1F",
|
||||
surfaceVariant: "#E7E0EC",
|
||||
surfaceVariantText: "#49454F",
|
||||
surface: "#fef7ff",
|
||||
surfaceText: "#1d1b20",
|
||||
surfaceVariant: "#e7e0eb",
|
||||
surfaceVariantText: "#49454e",
|
||||
surfaceTint: "#6750A4",
|
||||
background: "#FFFBFE",
|
||||
backgroundText: "#1C1B1F",
|
||||
outline: "#79747E",
|
||||
surfaceContainer: "#F3EDF7",
|
||||
surfaceContainerHigh: "#ECE6F0"
|
||||
background: "#fef7ff",
|
||||
backgroundText: "#1d1b20",
|
||||
outline: "#7a757f",
|
||||
surfaceContainer: "#f2ecf4",
|
||||
surfaceContainerHigh: "#ece6ee",
|
||||
surfaceContainerHighest: "#e6e0e9"
|
||||
},
|
||||
green: {
|
||||
name: "Green Light",
|
||||
@@ -232,16 +342,17 @@ const StockThemes = {
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#e8f5e8",
|
||||
secondary: "#4caf50",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surface: "#f7fbf1",
|
||||
surfaceText: "#191d17",
|
||||
surfaceVariant: "#dee5d8",
|
||||
surfaceVariantText: "#424940",
|
||||
surfaceTint: "#2e7d32",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
background: "#f7fbf1",
|
||||
backgroundText: "#191d17",
|
||||
outline: "#72796f",
|
||||
surfaceContainer: "#ecefe6",
|
||||
surfaceContainerHigh: "#e6e9e0",
|
||||
surfaceContainerHighest: "#e0e4db"
|
||||
},
|
||||
orange: {
|
||||
name: "Orange Light",
|
||||
@@ -249,16 +360,17 @@ const StockThemes = {
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#ffecb3",
|
||||
secondary: "#ff9800",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surface: "#fff8f6",
|
||||
surfaceText: "#221a16",
|
||||
surfaceVariant: "#f4ded5",
|
||||
surfaceVariantText: "#52443d",
|
||||
surfaceTint: "#e65100",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
background: "#fff8f6",
|
||||
backgroundText: "#221a16",
|
||||
outline: "#85736c",
|
||||
surfaceContainer: "#fceae3",
|
||||
surfaceContainerHigh: "#f6e5de",
|
||||
surfaceContainerHighest: "#f0dfd8"
|
||||
},
|
||||
red: {
|
||||
name: "Red Light",
|
||||
@@ -266,16 +378,17 @@ const StockThemes = {
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#ffebee",
|
||||
secondary: "#f44336",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surface: "#fff8f7",
|
||||
surfaceText: "#231918",
|
||||
surfaceVariant: "#f5ddda",
|
||||
surfaceVariantText: "#534341",
|
||||
surfaceTint: "#d32f2f",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
background: "#fff8f7",
|
||||
backgroundText: "#231918",
|
||||
outline: "#857370",
|
||||
surfaceContainer: "#fceae7",
|
||||
surfaceContainerHigh: "#f7e4e1",
|
||||
surfaceContainerHighest: "#f1dedc"
|
||||
},
|
||||
cyan: {
|
||||
name: "Cyan Light",
|
||||
@@ -283,16 +396,17 @@ const StockThemes = {
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#e0f2f1",
|
||||
secondary: "#00bcd4",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surface: "#f5fafc",
|
||||
surfaceText: "#171d1e",
|
||||
surfaceVariant: "#dbe4e6",
|
||||
surfaceVariantText: "#3f484a",
|
||||
surfaceTint: "#0097a7",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
background: "#f5fafc",
|
||||
backgroundText: "#171d1e",
|
||||
outline: "#6f797b",
|
||||
surfaceContainer: "#e9eff0",
|
||||
surfaceContainerHigh: "#e3e9eb",
|
||||
surfaceContainerHighest: "#dee3e5"
|
||||
},
|
||||
pink: {
|
||||
name: "Pink Light",
|
||||
@@ -300,16 +414,17 @@ const StockThemes = {
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#fce4ec",
|
||||
secondary: "#e91e63",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surface: "#fff8f7",
|
||||
surfaceText: "#22191a",
|
||||
surfaceVariant: "#f3dddf",
|
||||
surfaceVariantText: "#524345",
|
||||
surfaceTint: "#c2185b",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
background: "#fff8f7",
|
||||
backgroundText: "#22191a",
|
||||
outline: "#847375",
|
||||
surfaceContainer: "#fbeaeb",
|
||||
surfaceContainerHigh: "#f5e4e5",
|
||||
surfaceContainerHighest: "#f0dee0"
|
||||
},
|
||||
amber: {
|
||||
name: "Amber Light",
|
||||
@@ -317,16 +432,17 @@ const StockThemes = {
|
||||
primaryText: "#000000",
|
||||
primaryContainer: "#fff8e1",
|
||||
secondary: "#ffc107",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surface: "#fff8f2",
|
||||
surfaceText: "#1f1b13",
|
||||
surfaceVariant: "#ede1cf",
|
||||
surfaceVariantText: "#4d4639",
|
||||
surfaceTint: "#ff8f00",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
background: "#fff8f2",
|
||||
backgroundText: "#1f1b13",
|
||||
outline: "#7f7667",
|
||||
surfaceContainer: "#f6ecdf",
|
||||
surfaceContainerHigh: "#f1e7d9",
|
||||
surfaceContainerHighest: "#ebe1d4"
|
||||
},
|
||||
coral: {
|
||||
name: "Coral Light",
|
||||
@@ -334,23 +450,55 @@ const StockThemes = {
|
||||
primaryText: "#ffffff",
|
||||
primaryContainer: "#ffdad6",
|
||||
secondary: "#ff5449",
|
||||
surface: "#fefefe",
|
||||
surfaceText: "#1a1c1e",
|
||||
surfaceVariant: "#e7e0ec",
|
||||
surfaceVariantText: "#49454f",
|
||||
surface: "#fff8f7",
|
||||
surfaceText: "#231918",
|
||||
surfaceVariant: "#f5ddda",
|
||||
surfaceVariantText: "#534341",
|
||||
surfaceTint: "#8c1d18",
|
||||
background: "#fefefe",
|
||||
backgroundText: "#1a1c1e",
|
||||
outline: "#79747e",
|
||||
surfaceContainer: "#f3f3f3",
|
||||
surfaceContainerHigh: "#ececec"
|
||||
background: "#fff8f7",
|
||||
backgroundText: "#231918",
|
||||
outline: "#857371",
|
||||
surfaceContainer: "#fceae7",
|
||||
surfaceContainerHigh: "#f6e4e2",
|
||||
surfaceContainerHighest: "#f1dedc"
|
||||
},
|
||||
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 = {
|
||||
BLUE: "blue",
|
||||
DEEP_BLUE: "deepBlue",
|
||||
PURPLE: "purple",
|
||||
GREEN: "green",
|
||||
ORANGE: "orange",
|
||||
@@ -359,6 +507,7 @@ const ThemeNames = {
|
||||
PINK: "pink",
|
||||
AMBER: "amber",
|
||||
CORAL: "coral",
|
||||
MONOCHROME: "monochrome",
|
||||
DYNAMIC: "dynamic"
|
||||
}
|
||||
|
||||
@@ -366,15 +515,30 @@ function isStockTheme(themeName) {
|
||||
return Object.keys(StockThemes.DARK).includes(themeName)
|
||||
}
|
||||
|
||||
function isCatppuccinVariant(themeName) {
|
||||
return Object.keys(CatppuccinVariants).includes(themeName)
|
||||
}
|
||||
|
||||
function getAvailableThemes(isLight = false) {
|
||||
return isLight ? StockThemes.LIGHT : StockThemes.DARK
|
||||
}
|
||||
|
||||
function getThemeByName(themeName, isLight = false) {
|
||||
if (isCatppuccinVariant(themeName)) {
|
||||
return getCatppuccinTheme(themeName, isLight)
|
||||
}
|
||||
const themes = getAvailableThemes(isLight)
|
||||
return themes[themeName] || themes.blue
|
||||
}
|
||||
|
||||
function getAllThemeNames() {
|
||||
return Object.keys(StockThemes.DARK)
|
||||
}
|
||||
|
||||
function getCatppuccinVariantNames() {
|
||||
return Object.keys(CatppuccinVariants)
|
||||
}
|
||||
|
||||
function getThemeCategories() {
|
||||
return ThemeCategories
|
||||
}
|
||||
595
Common/Theme.qml
595
Common/Theme.qml
@@ -1,4 +1,5 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
@@ -6,41 +7,87 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "StockThemes.js" as StockThemes
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string currentTheme: "blue"
|
||||
property bool isLightMode: false
|
||||
|
||||
readonly property string dynamic: "dynamic"
|
||||
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
|
||||
|
||||
readonly property string homeDir: {
|
||||
const url = StandardPaths.writableLocation(StandardPaths.HomeLocation).toString()
|
||||
return url.startsWith("file://") ? url.substring(7) : url
|
||||
readonly property real popupDistance: 4
|
||||
|
||||
property string currentTheme: "blue"
|
||||
property string currentThemeCategory: "generic"
|
||||
property bool isLightMode: false
|
||||
|
||||
readonly property string dynamic: "dynamic"
|
||||
readonly property string custom : "custom"
|
||||
|
||||
readonly property string homeDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation))
|
||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||
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: {
|
||||
const url = StandardPaths.writableLocation(StandardPaths.ConfigLocation).toString()
|
||||
return url.startsWith("file://") ? url.substring(7) : url
|
||||
readonly property string rawWallpaperPath: {
|
||||
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)
|
||||
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 gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
|
||||
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
|
||||
property var workerRunning: false
|
||||
property var matugenColors: ({})
|
||||
property bool extractionRequested: false
|
||||
property int colorUpdateTrigger: 0
|
||||
property var customThemeData: null
|
||||
|
||||
readonly property string stateDir: {
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
|
||||
const path = cacheHome.startsWith("file://") ? cacheHome.substring(7) : cacheHome
|
||||
return path + "/dankshell"
|
||||
|
||||
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) {
|
||||
@@ -60,35 +107,61 @@ Singleton {
|
||||
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
|
||||
} else if (currentTheme === dynamic) {
|
||||
return {
|
||||
primary: getMatugenColor("primary", "#42a5f5"),
|
||||
primaryText: getMatugenColor("on_primary", "#ffffff"),
|
||||
primaryContainer: getMatugenColor("primary_container", "#1976d2"),
|
||||
secondary: getMatugenColor("secondary", "#8ab4f8"),
|
||||
surface: getMatugenColor("surface", "#1a1c1e"),
|
||||
surfaceText: getMatugenColor("on_background", "#e3e8ef"),
|
||||
surfaceVariant: getMatugenColor("surface_variant", "#44464f"),
|
||||
surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5"),
|
||||
surfaceTint: getMatugenColor("surface_tint", "#8ab4f8"),
|
||||
background: getMatugenColor("background", "#1a1c1e"),
|
||||
backgroundText: getMatugenColor("on_background", "#e3e8ef"),
|
||||
outline: getMatugenColor("outline", "#8e918f"),
|
||||
surfaceContainer: getMatugenColor("surface_container", "#1e2023"),
|
||||
surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f"),
|
||||
error: "#F2B8B5",
|
||||
warning: "#FF9800",
|
||||
info: "#2196F3",
|
||||
success: "#4CAF50"
|
||||
"primary": getMatugenColor("primary", "#42a5f5"),
|
||||
"primaryText": getMatugenColor("on_primary", "#ffffff"),
|
||||
"primaryContainer": getMatugenColor("primary_container", "#1976d2"),
|
||||
"secondary": getMatugenColor("secondary", "#8ab4f8"),
|
||||
"surface": getMatugenColor("surface", "#1a1c1e"),
|
||||
"surfaceText": getMatugenColor("on_background", "#e3e8ef"),
|
||||
"surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
|
||||
"surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"),
|
||||
"surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"),
|
||||
"background": getMatugenColor("background", "#1a1c1e"),
|
||||
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
|
||||
"outline": getMatugenColor("outline", "#8e918f"),
|
||||
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
|
||||
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
|
||||
"surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
|
||||
"error": "#F2B8B5",
|
||||
"warning": "#FF9800",
|
||||
"info": "#2196F3",
|
||||
"success": "#4CAF50"
|
||||
}
|
||||
} else {
|
||||
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 primaryText: currentThemeData.primaryText
|
||||
property color primaryContainer: currentThemeData.primaryContainer
|
||||
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 surfaceVariant: currentThemeData.surfaceVariant
|
||||
property color surfaceVariantText: currentThemeData.surfaceVariantText
|
||||
@@ -96,8 +169,32 @@ Singleton {
|
||||
property color background: currentThemeData.background
|
||||
property color backgroundText: currentThemeData.backgroundText
|
||||
property color outline: currentThemeData.outline
|
||||
property color surfaceContainer: currentThemeData.surfaceContainer
|
||||
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
|
||||
property color outlineVariant: currentThemeData.outlineVariant || Qt.rgba(outline.r, outline.g, outline.b, 0.6)
|
||||
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 warning: currentThemeData.warning || "#FF9800"
|
||||
@@ -130,6 +227,7 @@ Singleton {
|
||||
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 errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
||||
|
||||
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
||||
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
||||
@@ -148,10 +246,10 @@ Singleton {
|
||||
property real spacingM: 12
|
||||
property real spacingL: 16
|
||||
property real spacingXL: 24
|
||||
property real fontSizeSmall: 12
|
||||
property real fontSizeMedium: 14
|
||||
property real fontSizeLarge: 16
|
||||
property real fontSizeXLarge: 20
|
||||
property real fontSizeSmall: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 12
|
||||
property real fontSizeMedium: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 14
|
||||
property real fontSizeLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 16
|
||||
property real fontSizeXLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 20
|
||||
property real barHeight: 48
|
||||
property real iconSize: 24
|
||||
property real iconSizeSmall: 16
|
||||
@@ -161,41 +259,54 @@ Singleton {
|
||||
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85
|
||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92
|
||||
|
||||
function switchTheme(themeName, savePrefs = true) {
|
||||
function screenTransition() {
|
||||
CompositorService.isNiri && NiriService.doScreenTransition()
|
||||
}
|
||||
|
||||
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
||||
if (enableTransition) {
|
||||
screenTransition()
|
||||
}
|
||||
if (themeName === dynamic) {
|
||||
currentTheme = dynamic
|
||||
extractColors()
|
||||
} else if (themeName === "custom") {
|
||||
currentTheme = "custom"
|
||||
currentThemeCategory = dynamic
|
||||
} else if (themeName === custom) {
|
||||
currentTheme = custom
|
||||
currentThemeCategory = custom
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
|
||||
loadCustomThemeFromFile(SettingsData.customThemeFile)
|
||||
}
|
||||
} else {
|
||||
currentTheme = themeName
|
||||
// Determine category based on theme name
|
||||
if (StockThemes.isCatppuccinVariant(themeName)) {
|
||||
currentThemeCategory = "catppuccin"
|
||||
} else {
|
||||
currentThemeCategory = "generic"
|
||||
}
|
||||
}
|
||||
if (savePrefs && typeof SettingsData !== "undefined")
|
||||
SettingsData.setTheme(currentTheme)
|
||||
|
||||
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
|
||||
function setLightMode(light, savePrefs = true) {
|
||||
screenTransition()
|
||||
isLightMode = light
|
||||
if (savePrefs && typeof SessionData !== "undefined")
|
||||
SessionData.setLightMode(isLightMode)
|
||||
PortalService.setLightMode(isLightMode)
|
||||
PortalService.setLightMode(isLightMode)
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
|
||||
function toggleLightMode(savePrefs = true) {
|
||||
setLightMode(!isLightMode, savePrefs)
|
||||
}
|
||||
|
||||
|
||||
function forceGenerateSystemThemes() {
|
||||
screenTransition()
|
||||
if (!matugenAvailable) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showWarning("matugen not available - cannot generate system themes")
|
||||
}
|
||||
return
|
||||
}
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
@@ -217,7 +328,33 @@ Singleton {
|
||||
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) {
|
||||
screenTransition()
|
||||
if (themeData.dark || themeData.light) {
|
||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
||||
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
|
||||
@@ -225,7 +362,7 @@ Singleton {
|
||||
} else {
|
||||
customThemeData = themeData
|
||||
}
|
||||
|
||||
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
|
||||
@@ -237,7 +374,6 @@ Singleton {
|
||||
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
|
||||
property string currentThemeName: currentTheme
|
||||
|
||||
|
||||
function popupBackground() {
|
||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
||||
}
|
||||
@@ -250,8 +386,42 @@ Singleton {
|
||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
|
||||
}
|
||||
|
||||
function widgetBackground() {
|
||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
||||
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
|
||||
|
||||
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() {
|
||||
@@ -271,21 +441,34 @@ Singleton {
|
||||
return _getBatteryPowerProfileIcon()
|
||||
|
||||
if (isCharging) {
|
||||
if (level >= 90) return "battery_charging_full"
|
||||
if (level >= 80) return "battery_charging_90"
|
||||
if (level >= 60) return "battery_charging_80"
|
||||
if (level >= 50) return "battery_charging_60"
|
||||
if (level >= 30) return "battery_charging_50"
|
||||
if (level >= 20) return "battery_charging_30"
|
||||
if (level >= 90)
|
||||
return "battery_charging_full"
|
||||
if (level >= 80)
|
||||
return "battery_charging_90"
|
||||
if (level >= 60)
|
||||
return "battery_charging_80"
|
||||
if (level >= 50)
|
||||
return "battery_charging_60"
|
||||
if (level >= 30)
|
||||
return "battery_charging_50"
|
||||
if (level >= 20)
|
||||
return "battery_charging_30"
|
||||
return "battery_charging_20"
|
||||
} else {
|
||||
if (level >= 95) return "battery_full"
|
||||
if (level >= 85) return "battery_6_bar"
|
||||
if (level >= 70) return "battery_5_bar"
|
||||
if (level >= 55) return "battery_4_bar"
|
||||
if (level >= 40) return "battery_3_bar"
|
||||
if (level >= 25) return "battery_2_bar"
|
||||
if (level >= 10) return "battery_1_bar"
|
||||
if (level >= 95)
|
||||
return "battery_full"
|
||||
if (level >= 85)
|
||||
return "battery_6_bar"
|
||||
if (level >= 70)
|
||||
return "battery_5_bar"
|
||||
if (level >= 55)
|
||||
return "battery_4_bar"
|
||||
if (level >= 40)
|
||||
return "battery_3_bar"
|
||||
if (level >= 25)
|
||||
return "battery_2_bar"
|
||||
if (level >= 10)
|
||||
return "battery_1_bar"
|
||||
return "battery_alert"
|
||||
}
|
||||
}
|
||||
@@ -343,87 +526,97 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function extractColors() {
|
||||
extractionRequested = true
|
||||
if (matugenAvailable)
|
||||
fileChecker.running = true
|
||||
else
|
||||
matugenCheck.running = true
|
||||
}
|
||||
|
||||
function onLightModeChanged() {
|
||||
if (matugenColors && Object.keys(matugenColors).length > 0) {
|
||||
colorUpdateTrigger++
|
||||
}
|
||||
|
||||
|
||||
if (currentTheme === "custom" && customThemeFileView.path) {
|
||||
customThemeFileView.reload()
|
||||
}
|
||||
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
|
||||
function setDesiredTheme(kind, value, isLight, iconTheme) {
|
||||
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType) {
|
||||
if (!matugenAvailable) {
|
||||
console.warn("matugen not available - cannot set system theme")
|
||||
console.warn("matugen not available or disabled - cannot set system theme")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
||||
NiriService.suppressNextToast()
|
||||
}
|
||||
|
||||
const desired = {
|
||||
"kind": kind,
|
||||
"value": value,
|
||||
"mode": isLight ? "light" : "dark",
|
||||
"iconTheme": iconTheme || "System Default"
|
||||
"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`
|
||||
])
|
||||
|
||||
Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`])
|
||||
workerRunning = true
|
||||
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"]
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
function generateSystemThemesFromCurrentTheme() {
|
||||
if (!matugenAvailable)
|
||||
return
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||
|
||||
|
||||
if (currentTheme === dynamic) {
|
||||
if (!wallpaperPath) {
|
||||
return
|
||||
}
|
||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme)
|
||||
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) {
|
||||
console.warn("Custom theme data not available for system theme generation")
|
||||
return
|
||||
}
|
||||
primaryColor = customThemeData.primary
|
||||
matugenType = customThemeData.matugen_type
|
||||
} else {
|
||||
primaryColor = currentThemeData.primary
|
||||
matugenType = currentThemeData.matugen_type
|
||||
}
|
||||
|
||||
|
||||
if (!primaryColor) {
|
||||
console.warn("No primary color available for theme:", currentTheme)
|
||||
return
|
||||
}
|
||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme)
|
||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
||||
}
|
||||
}
|
||||
|
||||
function applyGtkColors() {
|
||||
if (!matugenAvailable) {
|
||||
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
|
||||
}
|
||||
@@ -436,7 +629,7 @@ Singleton {
|
||||
function applyQtColors() {
|
||||
if (!matugenAvailable) {
|
||||
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
|
||||
}
|
||||
@@ -446,173 +639,63 @@ Singleton {
|
||||
}
|
||||
|
||||
|
||||
function extractJsonFromText(text) {
|
||||
if (!text) return null
|
||||
|
||||
const start = text.search(/[{\[]/)
|
||||
if (start === -1) return null
|
||||
|
||||
const open = text[start]
|
||||
const pairs = { "{": '}', "[": ']' }
|
||||
const close = pairs[open]
|
||||
if (!close) return null
|
||||
|
||||
let inString = false
|
||||
let escape = false
|
||||
const stack = [open]
|
||||
|
||||
for (var i = start + 1; i < text.length; i++) {
|
||||
const ch = text[i]
|
||||
|
||||
if (inString) {
|
||||
if (escape) {
|
||||
escape = false
|
||||
} else if (ch === '\\') {
|
||||
escape = true
|
||||
} else if (ch === '"') {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (ch === '"') {
|
||||
inString = true
|
||||
continue
|
||||
}
|
||||
if (ch === '{' || ch === '[') {
|
||||
stack.push(ch)
|
||||
continue
|
||||
}
|
||||
if (ch === '}' || ch === ']') {
|
||||
const last = stack.pop()
|
||||
if (!last || pairs[last] !== ch) {
|
||||
return null
|
||||
}
|
||||
if (stack.length === 0) {
|
||||
return text.slice(start, i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
Process {
|
||||
id: matugenCheck
|
||||
command: ["which", "matugen"]
|
||||
onExited: code => {
|
||||
matugenAvailable = (code === 0)
|
||||
matugenAvailable = (code === 0) && !envDisableMatugen
|
||||
if (!matugenAvailable) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.wallpaperErrorStatus = "matugen_missing"
|
||||
ToastService.showWarning("matugen not found - dynamic theming disabled")
|
||||
}
|
||||
console.log("matugen not not available in path or disabled via DMS_DISABLE_MATUGEN")
|
||||
return
|
||||
}
|
||||
if (extractionRequested) {
|
||||
fileChecker.running = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||
|
||||
|
||||
if (currentTheme === dynamic) {
|
||||
if (wallpaperPath) {
|
||||
// Clear cache on startup to force regeneration
|
||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme)
|
||||
} else {
|
||||
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) {
|
||||
// Clear cache on startup to force regeneration
|
||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme)
|
||||
} else {
|
||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fileChecker
|
||||
command: ["test", "-r", wallpaperPath]
|
||||
onExited: code => {
|
||||
if (code === 0) {
|
||||
matugenProcess.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: matugenProcess
|
||||
command: ["matugen", "image", wallpaperPath, "--json", "hex"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: matugenCollector
|
||||
onStreamFinished: {
|
||||
if (!matugenCollector.text) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.wallpaperErrorStatus = "error"
|
||||
ToastService.showError("Wallpaper Processing Failed: Empty JSON extracted from matugen output.")
|
||||
}
|
||||
return
|
||||
}
|
||||
const extractedJson = extractJsonFromText(matugenCollector.text)
|
||||
if (!extractedJson) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.wallpaperErrorStatus = "error"
|
||||
ToastService.showError("Wallpaper Processing Failed: Invalid JSON extracted from matugen output.")
|
||||
}
|
||||
console.log("Raw matugen output:", matugenCollector.text)
|
||||
return
|
||||
}
|
||||
try {
|
||||
root.matugenColors = JSON.parse(extractedJson)
|
||||
root.colorUpdateTrigger++
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.clearWallpaperError()
|
||||
}
|
||||
} catch (e) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.wallpaperErrorStatus = "error"
|
||||
ToastService.showError("Wallpaper processing failed (JSON parse error after extraction)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: code => {
|
||||
if (code !== 0) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.wallpaperErrorStatus = "error"
|
||||
ToastService.showError("Matugen command failed with exit code " + code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ensureStateDir
|
||||
}
|
||||
|
||||
|
||||
|
||||
Process {
|
||||
id: systemThemeGenerator
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
workerRunning = false
|
||||
|
||||
|
||||
if (exitCode === 2) {
|
||||
// Exit code 2 means wallpaper/color not found - this is expected on first run
|
||||
console.log("Theme worker: wallpaper/color not found, skipping theme generation")
|
||||
@@ -639,7 +722,7 @@ Singleton {
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
|
||||
ToastService.showInfo("GTK colors applied successfully")
|
||||
}
|
||||
} else {
|
||||
@@ -675,16 +758,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Component.onCompleted: {
|
||||
matugenCheck.running = true
|
||||
if (typeof SessionData !== "undefined")
|
||||
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: customThemeFileView
|
||||
watchChanges: currentTheme === "custom"
|
||||
@@ -697,7 +770,7 @@ Singleton {
|
||||
ToastService.showError("Invalid JSON format: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onLoaded: {
|
||||
parseAndLoadTheme()
|
||||
}
|
||||
@@ -706,13 +779,55 @@ Singleton {
|
||||
customThemeFileView.reload()
|
||||
}
|
||||
|
||||
onLoadFailed: function(error) {
|
||||
onLoadFailed: function (error) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Failed to read theme file: " + error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: dynamicColorsFileView
|
||||
path: stateDir + "/dms-colors.json"
|
||||
watchChanges: currentTheme === dynamic
|
||||
|
||||
function parseAndLoadColors() {
|
||||
try {
|
||||
const colorsText = dynamicColorsFileView.text()
|
||||
if (colorsText) {
|
||||
root.matugenColors = JSON.parse(colorsText)
|
||||
root.colorUpdateTrigger++
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.clearWallpaperError()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.wallpaperErrorStatus = "error"
|
||||
ToastService.showError("Dynamic colors parse error: " + e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (currentTheme === dynamic) {
|
||||
parseAndLoadColors()
|
||||
}
|
||||
}
|
||||
|
||||
onFileChanged: {
|
||||
if (currentTheme === dynamic) {
|
||||
dynamicColorsFileView.reload()
|
||||
}
|
||||
}
|
||||
|
||||
onLoadFailed: function (error) {
|
||||
if (currentTheme === dynamic && typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Failed to read dynamic colors: " + error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "theme"
|
||||
|
||||
@@ -735,4 +850,4 @@ Singleton {
|
||||
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
|
||||
}
|
||||
166
Modals/Clipboard/ClipboardContent.qml
Normal file
166
Modals/Clipboard/ClipboardContent.qml
Normal file
@@ -0,0 +1,166 @@
|
||||
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: "transparent"
|
||||
clip: true
|
||||
|
||||
DankListView {
|
||||
id: clipboardListView
|
||||
anchors.fill: parent
|
||||
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
|
||||
}
|
||||
}
|
||||
130
Modals/Clipboard/ClipboardEntry.qml
Normal file
130
Modals/Clipboard/ClipboardEntry.qml
Normal file
@@ -0,0 +1,130 @@
|
||||
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 Theme.primaryPressed
|
||||
}
|
||||
return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
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,920 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: clipboardHistoryModal
|
||||
|
||||
property int totalCount: 0
|
||||
property var activeTheme: Theme
|
||||
property var clipboardEntries: []
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool showKeyboardHints: false
|
||||
property Component clipboardContent
|
||||
property int activeImageLoads: 0
|
||||
readonly property int maxConcurrentLoads: 3
|
||||
|
||||
function updateFilteredModel() {
|
||||
filteredClipboardModel.clear()
|
||||
for (var i = 0; i < clipboardModel.count; i++) {
|
||||
const entry = clipboardModel.get(i).entry
|
||||
if (searchText.trim().length === 0) {
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
})
|
||||
} else {
|
||||
const content = getEntryPreview(entry).toLowerCase()
|
||||
if (content.includes(searchText.toLowerCase()))
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
})
|
||||
}
|
||||
}
|
||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
||||
// Clamp selectedIndex to valid range
|
||||
if (filteredClipboardModel.count === 0) {
|
||||
keyboardNavigationActive = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
||||
selectedIndex = filteredClipboardModel.count - 1
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible)
|
||||
hide()
|
||||
else
|
||||
show()
|
||||
}
|
||||
|
||||
function show() {
|
||||
open()
|
||||
clipboardHistoryModal.searchText = ""
|
||||
clipboardHistoryModal.activeImageLoads = 0
|
||||
|
||||
initializeThumbnailSystem()
|
||||
refreshClipboard()
|
||||
keyboardController.reset()
|
||||
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.text = ""
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
clipboardHistoryModal.searchText = ""
|
||||
clipboardHistoryModal.activeImageLoads = 0
|
||||
|
||||
updateFilteredModel()
|
||||
keyboardController.reset()
|
||||
cleanupTempFiles()
|
||||
}
|
||||
|
||||
function initializeThumbnailSystem() {}
|
||||
|
||||
function cleanupTempFiles() {
|
||||
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
|
||||
}
|
||||
|
||||
function generateThumbnails() {}
|
||||
|
||||
function refreshClipboard() {
|
||||
clipboardProcess.running = true
|
||||
}
|
||||
|
||||
function copyEntry(entry) {
|
||||
const entryId = entry.split('\t')[0]
|
||||
Quickshell.execDetached(
|
||||
["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
|
||||
ToastService.showInfo("Copied to clipboard")
|
||||
hide()
|
||||
}
|
||||
|
||||
function deleteEntry(entry) {
|
||||
deleteProcess.deletedEntry = entry
|
||||
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(
|
||||
/'/g, "'\\''")}' | cliphist delete`]
|
||||
deleteProcess.running = true
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
clearProcess.running = true
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
let content = entry.replace(/^\s*\d+\s+/, "")
|
||||
if (content.includes("image/") || content.includes("binary data")
|
||||
|| /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
||||
const dimensionMatch = content.match(/(\d+)x(\d+)/)
|
||||
if (dimensionMatch)
|
||||
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
|
||||
|
||||
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
|
||||
if (typeMatch)
|
||||
return `Image (${typeMatch[1].toUpperCase()})`
|
||||
|
||||
return "Image"
|
||||
}
|
||||
if (content.length > 100)
|
||||
return content.substring(0, 100) + "..."
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
function getEntryType(entry) {
|
||||
if (entry.includes("image/") || entry.includes("binary data")
|
||||
|| /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry)
|
||||
|| /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry))
|
||||
return "image"
|
||||
|
||||
if (entry.length > 200)
|
||||
return "long_text"
|
||||
|
||||
return "text"
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: 650
|
||||
height: 550
|
||||
backgroundColor: Theme.popupBackground()
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
enableShadow: true
|
||||
onBackgroundClicked: {
|
||||
hide()
|
||||
}
|
||||
modalFocusScope.Keys.onPressed: function (event) {
|
||||
keyboardController.handleKey(event)
|
||||
}
|
||||
content: clipboardContent
|
||||
|
||||
QtObject {
|
||||
id: keyboardController
|
||||
|
||||
function reset() {
|
||||
selectedIndex = 0
|
||||
keyboardNavigationActive = false
|
||||
showKeyboardHints = false
|
||||
if (typeof clipboardListView !== 'undefined' && clipboardListView)
|
||||
clipboardListView.keyboardActive = false
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredClipboardModel.count === 0)
|
||||
return
|
||||
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1,
|
||||
filteredClipboardModel.count - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredClipboardModel.count === 0)
|
||||
return
|
||||
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
|
||||
function copySelected() {
|
||||
if (filteredClipboardModel.count === 0 || selectedIndex < 0
|
||||
|| selectedIndex >= filteredClipboardModel.count)
|
||||
return
|
||||
|
||||
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
|
||||
copyEntry(selectedEntry)
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
if (filteredClipboardModel.count === 0 || selectedIndex < 0
|
||||
|| selectedIndex >= filteredClipboardModel.count)
|
||||
return
|
||||
|
||||
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
|
||||
deleteEntry(selectedEntry)
|
||||
}
|
||||
|
||||
function handleKey(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (keyboardNavigationActive) {
|
||||
keyboardNavigationActive = false
|
||||
if (typeof clipboardListView !== 'undefined'
|
||||
&& clipboardListView)
|
||||
clipboardListView.keyboardActive = false
|
||||
|
||||
event.accepted = true
|
||||
} else {
|
||||
hide()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
if (!keyboardNavigationActive) {
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = 0
|
||||
if (typeof clipboardListView !== 'undefined'
|
||||
&& clipboardListView)
|
||||
clipboardListView.keyboardActive = true
|
||||
|
||||
event.accepted = true
|
||||
} else {
|
||||
selectNext()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
if (!keyboardNavigationActive) {
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = 0
|
||||
if (typeof clipboardListView !== 'undefined'
|
||||
&& clipboardListView)
|
||||
clipboardListView.keyboardActive = true
|
||||
|
||||
event.accepted = true
|
||||
} else if (selectedIndex === 0) {
|
||||
keyboardNavigationActive = false
|
||||
if (typeof clipboardListView !== 'undefined'
|
||||
&& clipboardListView)
|
||||
clipboardListView.keyboardActive = false
|
||||
|
||||
event.accepted = true
|
||||
} else {
|
||||
selectPrevious()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Delete
|
||||
&& (event.modifiers & Qt.ShiftModifier)) {
|
||||
clearAll()
|
||||
hide()
|
||||
event.accepted = true
|
||||
} else if (keyboardNavigationActive) {
|
||||
if ((event.key === Qt.Key_C
|
||||
&& (event.modifiers & Qt.ControlModifier))
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter) {
|
||||
copySelected()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Delete) {
|
||||
deleteSelected()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
if (event.key === Qt.Key_F10) {
|
||||
showKeyboardHints = !showKeyboardHints
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: clearConfirmDialog
|
||||
|
||||
confirmButtonText: "Clear All"
|
||||
confirmButtonColor: Theme.primary
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
clipboardHistoryModal.shouldHaveFocus = false
|
||||
} else if (clipboardHistoryModal.shouldBeVisible) {
|
||||
clipboardHistoryModal.shouldHaveFocus = true
|
||||
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
|
||||
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
|
||||
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: clipboardModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredClipboardModel
|
||||
}
|
||||
|
||||
Process {
|
||||
id: clipboardProcess
|
||||
|
||||
command: ["cliphist", "list"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
clipboardModel.clear()
|
||||
const lines = text.trim().split('\n')
|
||||
for (const line of lines) {
|
||||
if (line.trim().length > 0)
|
||||
clipboardModel.append({
|
||||
"entry": line
|
||||
})
|
||||
}
|
||||
updateFilteredModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: deleteProcess
|
||||
|
||||
property string deletedEntry: ""
|
||||
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
// Just remove the item from models instead of re-fetching everything
|
||||
for (var i = 0; i < clipboardModel.count; i++) {
|
||||
if (clipboardModel.get(
|
||||
i).entry === deleteProcess.deletedEntry) {
|
||||
clipboardModel.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
for (var j = 0; j < filteredClipboardModel.count; j++) {
|
||||
if (filteredClipboardModel.get(
|
||||
j).entry === deleteProcess.deletedEntry) {
|
||||
filteredClipboardModel.remove(j)
|
||||
break
|
||||
}
|
||||
}
|
||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
||||
// Clamp selectedIndex to valid range
|
||||
if (filteredClipboardModel.count === 0) {
|
||||
keyboardNavigationActive = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
||||
selectedIndex = filteredClipboardModel.count - 1
|
||||
}
|
||||
} else {
|
||||
console.warn("Failed to delete clipboard entry")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: clearProcess
|
||||
|
||||
command: ["cliphist", "wipe"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
clipboardModel.clear()
|
||||
filteredClipboardModel.clear()
|
||||
totalCount = 0
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
clipboardHistoryModal.show()
|
||||
return "CLIPBOARD_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close() {
|
||||
hide()
|
||||
return "CLIPBOARD_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
clipboardHistoryModal.toggle()
|
||||
return "CLIPBOARD_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "clipboard"
|
||||
}
|
||||
|
||||
clipboardContent: Component {
|
||||
Item {
|
||||
id: clipboardContent
|
||||
property alias searchField: searchField
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
focus: false
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "content_paste"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `Clipboard History (${totalCount})`
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
||||
hoverColor: Theme.primaryHover
|
||||
onClicked: {
|
||||
showKeyboardHints = !showKeyboardHints
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "delete_sweep"
|
||||
iconSize: Theme.iconSize
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Theme.surfaceHover
|
||||
onClicked: {
|
||||
clearConfirmDialog.show(
|
||||
"Clear All History?",
|
||||
"This will permanently delete all clipboard history.",
|
||||
function() {
|
||||
clearAll()
|
||||
hide()
|
||||
},
|
||||
function() {} // No action on cancel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Theme.surfaceHover
|
||||
onClicked: hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width
|
||||
placeholderText: ""
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
focus: true
|
||||
ignoreLeftRightKeys: true
|
||||
keyForwardTargets: [modalFocusScope]
|
||||
onTextChanged: {
|
||||
clipboardHistoryModal.searchText = text
|
||||
updateFilteredModel()
|
||||
}
|
||||
Keys.onEscapePressed: function (event) {
|
||||
hide()
|
||||
event.accepted = true
|
||||
}
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(function () {
|
||||
forceActiveFocus()
|
||||
})
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: clipboardHistoryModal
|
||||
function onOpened() {
|
||||
Qt.callLater(function () {
|
||||
searchField.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - 110
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceLight
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
DankListView {
|
||||
id: clipboardListView
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
var itemHeight = 72 + spacing
|
||||
var itemY = index * itemHeight
|
||||
var itemBottom = itemY + itemHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
clip: true
|
||||
model: filteredClipboardModel
|
||||
currentIndex: selectedIndex
|
||||
spacing: Theme.spacingXS
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No clipboard entries found"
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: filteredClipboardModel.count === 0
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
property string entryType: getEntryType(model.entry)
|
||||
property string entryPreview: getEntryPreview(
|
||||
model.entry)
|
||||
property int entryIndex: index + 1
|
||||
property string entryData: model.entry
|
||||
property alias thumbnailImageSource: thumbnailImageSource
|
||||
|
||||
width: clipboardListView.width
|
||||
height: 72
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (keyboardNavigationActive
|
||||
&& index === selectedIndex)
|
||||
return Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b, 0.2)
|
||||
|
||||
return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
|
||||
}
|
||||
border.color: {
|
||||
if (keyboardNavigationActive
|
||||
&& index === selectedIndex)
|
||||
return Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b, 0.5)
|
||||
|
||||
return Theme.outlineStrong
|
||||
}
|
||||
border.width: keyboardNavigationActive
|
||||
&& index === selectedIndex ? 1.5 : 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: Theme.primarySelected
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: entryIndex.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 68
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: entryType === "image" ? 48 : Theme.iconSize
|
||||
height: entryType === "image" ? 48 : Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Image {
|
||||
id: thumbnailImageSource
|
||||
|
||||
property string entryId: model.entry.split(
|
||||
'\t')[0]
|
||||
property bool isVisible: false
|
||||
property string cachedImageData: ""
|
||||
property bool loadQueued: false
|
||||
|
||||
anchors.fill: parent
|
||||
source: ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
cache: false // Disable Qt's cache to control it ourselves
|
||||
visible: false
|
||||
asynchronous: true
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
|
||||
onCachedImageDataChanged: {
|
||||
if (cachedImageData) {
|
||||
source = ""
|
||||
source = `data:image/png;base64,${cachedImageData}`
|
||||
}
|
||||
}
|
||||
|
||||
function tryLoadImage() {
|
||||
if (!loadQueued && entryType === "image" && !cachedImageData) {
|
||||
loadQueued = true
|
||||
if (clipboardHistoryModal.activeImageLoads < clipboardHistoryModal.maxConcurrentLoads) {
|
||||
clipboardHistoryModal.activeImageLoads++
|
||||
imageLoader.running = true
|
||||
} else {
|
||||
// Retry after delay
|
||||
retryTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: retryTimer
|
||||
interval: 50
|
||||
onTriggered: {
|
||||
if (thumbnailImageSource.loadQueued && !imageLoader.running) {
|
||||
if (clipboardHistoryModal.activeImageLoads < clipboardHistoryModal.maxConcurrentLoads) {
|
||||
clipboardHistoryModal.activeImageLoads++
|
||||
imageLoader.running = true
|
||||
} else {
|
||||
retryTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (entryType !== "image") return
|
||||
|
||||
// Check if item is visible on screen initially
|
||||
let itemY = index * (72 + clipboardListView.spacing)
|
||||
let viewTop = clipboardListView.contentY
|
||||
let viewBottom = viewTop + clipboardListView.height
|
||||
isVisible = (itemY + 72 >= viewTop && itemY <= viewBottom)
|
||||
|
||||
if (isVisible) {
|
||||
tryLoadImage()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: clipboardListView
|
||||
function onContentYChanged() {
|
||||
if (entryType !== "image") return
|
||||
|
||||
let itemY = index * (72 + clipboardListView.spacing)
|
||||
let viewTop = clipboardListView.contentY - 100 // Preload slightly before visible
|
||||
let viewBottom = viewTop + clipboardListView.height + 200
|
||||
let nowVisible = (itemY + 72 >= viewTop && itemY <= viewBottom)
|
||||
|
||||
if (nowVisible && !thumbnailImageSource.isVisible) {
|
||||
thumbnailImageSource.isVisible = true
|
||||
thumbnailImageSource.tryLoadImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: imageLoader
|
||||
|
||||
running: false
|
||||
|
||||
command: ["sh", "-c", `cliphist decode ${thumbnailImageSource.entryId} | base64 -w 0`]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let imageData = text.trim()
|
||||
if (imageData && imageData.length > 0) {
|
||||
thumbnailImageSource.cachedImageData = imageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
thumbnailImageSource.loadQueued = false
|
||||
if (clipboardHistoryModal.activeImageLoads > 0) {
|
||||
clipboardHistoryModal.activeImageLoads--
|
||||
}
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Failed to load clipboard image:", thumbnailImageSource.entryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: thumbnailImageSource
|
||||
maskEnabled: true
|
||||
maskSource: clipboardCircularMask
|
||||
visible: entryType === "image"
|
||||
&& thumbnailImageSource.status === Image.Ready
|
||||
&& thumbnailImageSource.source != ""
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: clipboardCircularMask
|
||||
|
||||
width: 48 - 4
|
||||
height: 48 - 4
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
visible: !(entryType === "image"
|
||||
&& thumbnailImageSource.status === Image.Ready
|
||||
&& thumbnailImageSource.source != "")
|
||||
name: {
|
||||
if (entryType === "image")
|
||||
return "image"
|
||||
|
||||
if (entryType === "long_text")
|
||||
return "subject"
|
||||
|
||||
return "content_copy"
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (entryType === "image" ? 48 : Theme.iconSize) - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
switch (entryType) {
|
||||
case "image":
|
||||
return "Image • " + entryPreview
|
||||
case "long_text":
|
||||
return "Long Text"
|
||||
default:
|
||||
return "Text"
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: contentText
|
||||
|
||||
text: entryPreview
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: entryType === "long_text" ? 3 : 1
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Theme.surfaceHover
|
||||
onClicked: {
|
||||
deleteEntry(model.entry)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyEntry(model.entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: showKeyboardHints ? 80 + Theme.spacingL : 0
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r,
|
||||
Theme.surfaceContainer.g,
|
||||
Theme.surfaceContainer.b, 0.95)
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
opacity: showKeyboardHints ? 1 : 0
|
||||
z: 100
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Shift+Del: Clear All • Esc: Close"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modals.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
@@ -14,10 +11,9 @@ DankModal {
|
||||
property string confirmButtonText: "Confirm"
|
||||
property string cancelButtonText: "Cancel"
|
||||
property color confirmButtonColor: Theme.primary
|
||||
property var onConfirm: function() {}
|
||||
property var onCancel: function() {}
|
||||
|
||||
property int selectedButton: -1 // -1 = none, 0 = Cancel, 1 = Confirm
|
||||
property var onConfirm: function () {}
|
||||
property var onCancel: function () {}
|
||||
property int selectedButton: -1
|
||||
property bool keyboardNavigation: false
|
||||
|
||||
function show(title, message, onConfirmCallback, onCancelCallback) {
|
||||
@@ -26,33 +22,36 @@ DankModal {
|
||||
confirmButtonText = "Confirm"
|
||||
cancelButtonText = "Cancel"
|
||||
confirmButtonColor = Theme.primary
|
||||
onConfirm = onConfirmCallback || function() {}
|
||||
onCancel = onCancelCallback || function() {}
|
||||
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 || function() {}
|
||||
onCancel = options.onCancel || function() {}
|
||||
onConfirm = options.onConfirm || (() => {})
|
||||
onCancel = options.onCancel || (() => {})
|
||||
selectedButton = -1
|
||||
keyboardNavigation = false
|
||||
open()
|
||||
}
|
||||
|
||||
function selectButton() {
|
||||
close()
|
||||
if (selectedButton === 0) {
|
||||
close()
|
||||
if (onCancel) onCancel()
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
} else {
|
||||
close()
|
||||
if (onConfirm) onConfirm()
|
||||
if (onConfirm) {
|
||||
onConfirm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,18 +63,22 @@ DankModal {
|
||||
shouldHaveFocus: true
|
||||
onBackgroundClicked: {
|
||||
close()
|
||||
if (onCancel) onCancel()
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
onOpened: {
|
||||
modalFocusScope.forceActiveFocus()
|
||||
modalFocusScope.focus = true
|
||||
shouldHaveFocus = true
|
||||
}
|
||||
modalFocusScope.Keys.onPressed: function(event) {
|
||||
modalFocusScope.Keys.onPressed: function (event) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
close()
|
||||
if (onCancel) onCancel()
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Left:
|
||||
@@ -148,12 +151,13 @@ DankModal {
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (keyboardNavigation && selectedButton === 0)
|
||||
if (keyboardNavigation && selectedButton === 0) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
else if (cancelButton.containsMouse)
|
||||
} else if (cancelButton.containsMouse) {
|
||||
return Theme.surfacePressed
|
||||
else
|
||||
} else {
|
||||
return Theme.surfaceVariantAlpha
|
||||
}
|
||||
}
|
||||
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
|
||||
border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
|
||||
@@ -177,7 +181,6 @@ DankModal {
|
||||
selectButton()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -185,13 +188,14 @@ DankModal {
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
let baseColor = confirmButtonColor
|
||||
if (keyboardNavigation && selectedButton === 1)
|
||||
const baseColor = confirmButtonColor
|
||||
if (keyboardNavigation && selectedButton === 1) {
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
|
||||
else if (confirmButton.containsMouse)
|
||||
} else if (confirmButton.containsMouse) {
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
|
||||
else
|
||||
} else {
|
||||
return baseColor
|
||||
}
|
||||
}
|
||||
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
|
||||
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
|
||||
@@ -215,15 +219,9 @@ DankModal {
|
||||
selectButton()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import qs.Common
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
WlrLayershell.namespace: "quickshell:modal"
|
||||
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property real width: 400
|
||||
@@ -27,7 +29,6 @@ PanelWindow {
|
||||
property real borderWidth: 1
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
property bool enableShadow: false
|
||||
// Expose the focusScope for external access
|
||||
property alias modalFocusScope: focusScope
|
||||
property bool shouldBeVisible: false
|
||||
property bool shouldHaveFocus: shouldBeVisible
|
||||
@@ -38,15 +39,6 @@ PanelWindow {
|
||||
signal dialogClosed
|
||||
signal backgroundClicked
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
ModalManager.openModal(root)
|
||||
closeTimer.stop()
|
||||
@@ -61,15 +53,16 @@ PanelWindow {
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible)
|
||||
if (shouldBeVisible) {
|
||||
close()
|
||||
else
|
||||
} else {
|
||||
open()
|
||||
}
|
||||
}
|
||||
|
||||
visible: shouldBeVisible
|
||||
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.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
onVisibleChanged: {
|
||||
@@ -84,6 +77,16 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
target: ModalManager
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
|
||||
@@ -112,13 +115,10 @@ PanelWindow {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick
|
||||
onClicked: mouse => {
|
||||
var localPos = mapToItem(contentContainer,
|
||||
mouse.x, mouse.y)
|
||||
if (localPos.x < 0
|
||||
|| localPos.x > contentContainer.width
|
||||
|| localPos.y < 0
|
||||
|| localPos.y > contentContainer.height)
|
||||
root.backgroundClicked()
|
||||
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
||||
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
||||
root.backgroundClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,19 +137,20 @@ PanelWindow {
|
||||
height: root.height
|
||||
anchors.centerIn: positioning === "center" ? parent : undefined
|
||||
x: {
|
||||
if (positioning === "top-right")
|
||||
return Math.max(Theme.spacingL,
|
||||
root.screenWidth - width - Theme.spacingL)
|
||||
else if (positioning === "custom")
|
||||
if (positioning === "top-right") {
|
||||
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL)
|
||||
} else if (positioning === "custom") {
|
||||
return root.customPosition.x
|
||||
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
|
||||
}
|
||||
return 0
|
||||
}
|
||||
y: {
|
||||
if (positioning === "top-right")
|
||||
if (positioning === "top-right") {
|
||||
return Theme.barHeight + Theme.spacingXS
|
||||
else if (positioning === "custom")
|
||||
} else if (positioning === "custom") {
|
||||
return root.customPosition.y
|
||||
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
|
||||
}
|
||||
return 0
|
||||
}
|
||||
color: root.backgroundColor
|
||||
radius: root.cornerRadius
|
||||
@@ -157,12 +158,7 @@ PanelWindow {
|
||||
border.width: root.borderWidth
|
||||
layer.enabled: root.enableShadow
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
scale: {
|
||||
if (root.animationType === "scale")
|
||||
return root.shouldBeVisible ? 1 : 0.9
|
||||
|
||||
return 1
|
||||
}
|
||||
scale: root.animationType === "scale" ? (root.shouldBeVisible ? 1 : 0.9) : 1
|
||||
transform: root.animationType === "slide" ? slideTransform : null
|
||||
|
||||
Translate {
|
||||
@@ -214,28 +210,25 @@ PanelWindow {
|
||||
visible: root.visible // Only active when the modal is visible
|
||||
focus: root.visible
|
||||
Keys.onEscapePressed: event => {
|
||||
if (root.closeOnEscapeKey
|
||||
&& shouldHaveFocus) {
|
||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (visible && shouldHaveFocus)
|
||||
Qt.callLater(function () {
|
||||
focusScope.forceActiveFocus()
|
||||
})
|
||||
if (visible && shouldHaveFocus) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldHaveFocusChanged() {
|
||||
if (shouldHaveFocus && visible) {
|
||||
Qt.callLater(function () {
|
||||
focusScope.forceActiveFocus()
|
||||
})
|
||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtCore
|
||||
import Qt.labs.folderlistmodel
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
@@ -16,8 +14,7 @@ Rectangle {
|
||||
|
||||
height: 200
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
||||
Theme.surfaceContainer.b, 0.95)
|
||||
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
|
||||
@@ -25,7 +22,7 @@ Rectangle {
|
||||
|
||||
onShowFileInfoChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
var fullPath = currentPath + "/" + currentFileName
|
||||
const fullPath = currentPath + "/" + currentFileName
|
||||
fileStatProcess.selectedFilePath = fullPath
|
||||
fileStatProcess.running = true
|
||||
}
|
||||
@@ -37,64 +34,24 @@ Rectangle {
|
||||
property string selectedFilePath: ""
|
||||
property var fileStats: null
|
||||
running: false
|
||||
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
var parts = text.trim().split('|')
|
||||
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]
|
||||
"modifiedTime": parts[0],
|
||||
"permissions": parts[1],
|
||||
"size": parseInt(parts[2]) || 0,
|
||||
"fullPath": parts[3]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {}
|
||||
}
|
||||
|
||||
readonly property var currentFileData: {
|
||||
if (!sourceFolderModel || selectedIndex < 0 || selectedIndex >= sourceFolderModel.count) {
|
||||
return {
|
||||
exists: false,
|
||||
name: "No selection",
|
||||
type: "",
|
||||
size: "",
|
||||
modified: "",
|
||||
permissions: "",
|
||||
extension: "",
|
||||
position: selectedIndex >= 0 ? (selectedIndex + 1) + " of " + (sourceFolderModel ? sourceFolderModel.count : 0) : "N/A"
|
||||
}
|
||||
}
|
||||
|
||||
var path = currentPath
|
||||
if (path && selectedIndex >= 0) {
|
||||
return {
|
||||
exists: true,
|
||||
name: "Loading...",
|
||||
type: "file",
|
||||
size: "Calculating...",
|
||||
modified: "Loading...",
|
||||
permissions: "Loading...",
|
||||
extension: "",
|
||||
position: (selectedIndex + 1) + " of " + sourceFolderModel.count
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exists: false,
|
||||
name: "No selection",
|
||||
type: "",
|
||||
size: "",
|
||||
modified: "",
|
||||
permissions: "",
|
||||
extension: "",
|
||||
position: "N/A"
|
||||
}
|
||||
onExited: function (exitCode) {}
|
||||
}
|
||||
|
||||
property string currentFileName: ""
|
||||
@@ -103,7 +60,7 @@ Rectangle {
|
||||
|
||||
onCurrentFileNameChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
var fullPath = currentPath + "/" + currentFileName
|
||||
const fullPath = currentPath + "/" + currentFileName
|
||||
if (fullPath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = fullPath
|
||||
fileStatProcess.running = true
|
||||
@@ -116,16 +73,16 @@ Rectangle {
|
||||
fileStatProcess.selectedFilePath = filePath
|
||||
currentFileName = fileName || ""
|
||||
currentFileIsDir = isDirectory || false
|
||||
|
||||
var ext = ""
|
||||
|
||||
let ext = ""
|
||||
if (!isDirectory && fileName) {
|
||||
var lastDot = fileName.lastIndexOf('.')
|
||||
const lastDot = fileName.lastIndexOf('.')
|
||||
if (lastDot > 0) {
|
||||
ext = fileName.substring(lastDot + 1).toLowerCase()
|
||||
}
|
||||
}
|
||||
currentFileExtension = ext
|
||||
|
||||
|
||||
if (showFileInfo) {
|
||||
fileStatProcess.running = true
|
||||
}
|
||||
@@ -135,27 +92,27 @@ Rectangle {
|
||||
readonly property var currentFileDisplayData: {
|
||||
if (selectedIndex < 0 || !sourceFolderModel) {
|
||||
return {
|
||||
exists: false,
|
||||
name: "No selection",
|
||||
type: "",
|
||||
size: "",
|
||||
modified: "",
|
||||
permissions: "",
|
||||
extension: "",
|
||||
position: "N/A"
|
||||
"exists": false,
|
||||
"name": "No selection",
|
||||
"type": "",
|
||||
"size": "",
|
||||
"modified": "",
|
||||
"permissions": "",
|
||||
"extension": "",
|
||||
"position": "N/A"
|
||||
}
|
||||
}
|
||||
|
||||
var hasValidFile = currentFileName !== ""
|
||||
|
||||
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"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,16 +208,20 @@ Rectangle {
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0 || !bytes) return "0 B"
|
||||
var k = 1024
|
||||
var sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
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"
|
||||
var parts = dateTimeString.split(' ')
|
||||
if (!dateTimeString) {
|
||||
return "Unknown"
|
||||
}
|
||||
const parts = dateTimeString.split(' ')
|
||||
if (parts.length >= 2) {
|
||||
return parts[0] + " " + parts[1].split('.')[0]
|
||||
}
|
||||
@@ -273,4 +234,4 @@ Rectangle {
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,7 @@ Rectangle {
|
||||
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
||||
Theme.surfaceContainer.b, 0.95)
|
||||
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
|
||||
@@ -48,4 +47,4 @@ Rectangle {
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -13,7 +11,6 @@ DankModal {
|
||||
property bool networkInfoModalVisible: false
|
||||
property string networkSSID: ""
|
||||
property var networkData: null
|
||||
property string networkDetails: ""
|
||||
|
||||
function showNetworkInfo(ssid, data) {
|
||||
networkSSID = ssid
|
||||
@@ -28,21 +25,17 @@ DankModal {
|
||||
close()
|
||||
networkSSID = ""
|
||||
networkData = null
|
||||
networkDetails = ""
|
||||
}
|
||||
|
||||
visible: networkInfoModalVisible
|
||||
width: 600
|
||||
height: 500
|
||||
enableShadow: true
|
||||
onBackgroundClicked: {
|
||||
hideDialog()
|
||||
}
|
||||
onBackgroundClicked: hideDialog()
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
networkSSID = ""
|
||||
networkData = null
|
||||
networkDetails = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,58 +63,51 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Details for \"" + networkSSID + "\""
|
||||
text: `Details for "${networkSSID}"`
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Theme.errorHover
|
||||
onClicked: {
|
||||
root.hideDialog()
|
||||
}
|
||||
onClicked: root.hideDialog()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: flickableArea
|
||||
Rectangle {
|
||||
id: detailsRect
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - 140
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: detailsRect.height
|
||||
|
||||
Rectangle {
|
||||
id: detailsRect
|
||||
width: parent.width
|
||||
height: detailsText.contentHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
contentHeight: detailsText.contentHeight
|
||||
|
||||
TextArea {
|
||||
StyledText {
|
||||
id: detailsText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
text: NetworkService.networkInfoDetails.replace(
|
||||
/\\n/g,
|
||||
'\n') || "No information available"
|
||||
|
||||
width: parent.width
|
||||
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.WordWrap
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
background: null
|
||||
padding: 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -131,14 +117,10 @@ DankModal {
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.max(
|
||||
70,
|
||||
closeText.contentWidth + Theme.spacingM * 2)
|
||||
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: closeArea.containsMouse ? Qt.darker(
|
||||
Theme.primary,
|
||||
1.1) : Theme.primary
|
||||
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
id: closeText
|
||||
@@ -156,9 +138,7 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hideDialog()
|
||||
}
|
||||
onClicked: root.hideDialog()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
@@ -166,10 +146,17 @@ DankModal {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property bool notepadModalVisible: false
|
||||
property bool fileDialogOpen: false
|
||||
property string currentFileName: "" // Track the currently loaded file
|
||||
|
||||
function show() {
|
||||
notepadModalVisible = true;
|
||||
shouldHaveFocus = Qt.binding(() => {
|
||||
return notepadModalVisible && !fileDialogOpen;
|
||||
});
|
||||
open();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
notepadModalVisible = false;
|
||||
// Clear filename when closing (so it doesn't persist between sessions)
|
||||
currentFileName = "";
|
||||
close();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (notepadModalVisible)
|
||||
hide();
|
||||
else
|
||||
show();
|
||||
}
|
||||
|
||||
visible: notepadModalVisible
|
||||
width: 700
|
||||
height: 500
|
||||
enableShadow: true
|
||||
onShouldHaveFocusChanged: {
|
||||
console.log("Notepad: shouldHaveFocus changed to", shouldHaveFocus, "modalVisible:", notepadModalVisible, "dialogOpen:", fileDialogOpen);
|
||||
}
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
id: contentItem
|
||||
|
||||
anchors.fill: parent
|
||||
property alias textArea: textArea
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onNotepadModalVisibleChanged() {
|
||||
if (root.notepadModalVisible) {
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
width: parent.width - closeButton.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: "Notepad"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileName
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
visible: currentFileName !== ""
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
width: 200
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SessionData.notepadContent.length > 0 ? `${SessionData.notepadContent.length} characters` : "Empty"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: closeButton
|
||||
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Theme.errorHover
|
||||
onClicked: root.hide()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: parent.height - 80
|
||||
color: Theme.surface
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
clip: true
|
||||
|
||||
TextArea {
|
||||
id: textArea
|
||||
|
||||
text: SessionData.notepadContent
|
||||
placeholderText: "Start typing your notes here..."
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
wrapMode: TextArea.Wrap
|
||||
focus: root.notepadModalVisible
|
||||
activeFocusOnTab: true
|
||||
|
||||
onTextChanged: {
|
||||
if (text !== SessionData.notepadContent) {
|
||||
SessionData.notepadContent = text;
|
||||
saveTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: (event) => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.notepadModalVisible) {
|
||||
Qt.callLater(() => {
|
||||
forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "save"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.primary
|
||||
hoverColor: Theme.primaryHover
|
||||
onClicked: {
|
||||
console.log("Notepad: Opening save dialog, releasing modal focus");
|
||||
root.allowFocusOverride = true;
|
||||
root.shouldHaveFocus = false;
|
||||
fileDialogOpen = true;
|
||||
saveBrowser.open();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Save to file"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "folder_open"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.secondary
|
||||
hoverColor: Theme.secondaryHover
|
||||
onClicked: {
|
||||
console.log("Notepad: Opening load dialog, releasing modal focus");
|
||||
root.allowFocusOverride = true;
|
||||
root.shouldHaveFocus = false;
|
||||
fileDialogOpen = true;
|
||||
loadBrowser.open();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Load file"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: saveTimer.running ? "Auto-saving..." : "Auto-saved"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: saveTimer.running ? Theme.primary : Theme.surfaceTextMedium
|
||||
opacity: SessionData.notepadContent.length > 0 ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: SessionData.saveSettings()
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: saveBrowser
|
||||
|
||||
browserTitle: "Save Notepad File"
|
||||
browserIcon: "save"
|
||||
browserType: "notepad_save"
|
||||
fileExtensions: ["*.txt", "*.*"]
|
||||
allowStacking: true
|
||||
saveMode: true
|
||||
defaultFileName: "note.txt"
|
||||
|
||||
onFileSelected: (path) => {
|
||||
fileDialogOpen = false;
|
||||
selectedFilePath = path;
|
||||
const content = textArea.text;
|
||||
if (content.length > 0) {
|
||||
writeFileProcess.command = ["sh", "-c", `echo '${content.replace(/'/g, "'\\''")}' > '${path}'`];
|
||||
writeFileProcess.running = true;
|
||||
}
|
||||
close();
|
||||
// Restore modal focus
|
||||
root.allowFocusOverride = false;
|
||||
root.shouldHaveFocus = Qt.binding(() => {
|
||||
return root.notepadModalVisible && !fileDialogOpen;
|
||||
});
|
||||
// Restore focus to TextArea after dialog closes
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
fileDialogOpen = false;
|
||||
// Restore modal focus
|
||||
root.allowFocusOverride = false;
|
||||
root.shouldHaveFocus = Qt.binding(() => {
|
||||
return root.notepadModalVisible && !fileDialogOpen;
|
||||
});
|
||||
// Restore focus to TextArea after dialog closes
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
property string selectedFilePath: ""
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: loadBrowser
|
||||
|
||||
browserTitle: "Load Notepad File"
|
||||
browserIcon: "folder_open"
|
||||
browserType: "notepad_load"
|
||||
fileExtensions: ["*.txt", "*.*"]
|
||||
allowStacking: true
|
||||
|
||||
onFileSelected: (path) => {
|
||||
fileDialogOpen = false;
|
||||
// Clean the file path - remove file:// prefix if present
|
||||
var cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
// Extract filename from path
|
||||
var fileName = cleanPath.split('/').pop();
|
||||
currentFileName = fileName;
|
||||
console.log("Notepad: Loading file from path:", cleanPath);
|
||||
readFileProcess.command = ["cat", cleanPath];
|
||||
readFileProcess.running = true;
|
||||
close();
|
||||
// Restore modal focus
|
||||
root.allowFocusOverride = false;
|
||||
root.shouldHaveFocus = Qt.binding(() => {
|
||||
return root.notepadModalVisible && !fileDialogOpen;
|
||||
});
|
||||
// Restore focus to TextArea after dialog closes
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
fileDialogOpen = false;
|
||||
// Restore modal focus
|
||||
root.allowFocusOverride = false;
|
||||
root.shouldHaveFocus = Qt.binding(() => {
|
||||
return root.notepadModalVisible && !fileDialogOpen;
|
||||
});
|
||||
// Restore focus to TextArea after dialog closes
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: writeFileProcess
|
||||
|
||||
command: []
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
console.log("Notepad: File saved successfully");
|
||||
else
|
||||
console.warn("Notepad: Failed to save file, exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: readFileProcess
|
||||
|
||||
command: []
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
console.log("Notepad: File content loaded, length:", text.length);
|
||||
textArea.text = text;
|
||||
SessionData.notepadContent = text;
|
||||
SessionData.saveSettings();
|
||||
console.log("Notepad: File loaded and saved to session");
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
console.log("Notepad: File read process exited with code:", exitCode);
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Notepad: Failed to load file, exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modules.Notifications.Center
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -12,6 +9,47 @@ import qs.Widgets
|
||||
DankModal {
|
||||
id: notificationModal
|
||||
|
||||
property bool notificationModalOpen: false
|
||||
property var notificationListRef: null
|
||||
|
||||
function show() {
|
||||
notificationModalOpen = true
|
||||
NotificationService.onOverlayOpen()
|
||||
open()
|
||||
modalKeyboardController.reset()
|
||||
if (modalKeyboardController && notificationListRef) {
|
||||
modalKeyboardController.listView = notificationListRef
|
||||
modalKeyboardController.rebuildFlatNavigation()
|
||||
|
||||
Qt.callLater(() => {
|
||||
modalKeyboardController.keyboardNavigationActive = true
|
||||
modalKeyboardController.selectedFlatIndex = 0
|
||||
modalKeyboardController.updateSelectedIdFromIndex()
|
||||
if (notificationListRef) {
|
||||
notificationListRef.keyboardActive = true
|
||||
notificationListRef.currentIndex = 0
|
||||
}
|
||||
modalKeyboardController.selectionVersion++
|
||||
modalKeyboardController.ensureVisible()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
notificationModalOpen = false
|
||||
NotificationService.onOverlayClose()
|
||||
close()
|
||||
modalKeyboardController.reset()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
width: 500
|
||||
height: 700
|
||||
visible: false
|
||||
@@ -23,69 +61,36 @@ DankModal {
|
||||
NotificationService.onOverlayClose()
|
||||
}
|
||||
}
|
||||
|
||||
modalFocusScope.Keys.onPressed: function (event) {
|
||||
modalKeyboardController.handleKey(event)
|
||||
}
|
||||
modalFocusScope.Keys.onPressed: (event) => modalKeyboardController.handleKey(event)
|
||||
|
||||
NotificationKeyboardController {
|
||||
id: modalKeyboardController
|
||||
|
||||
listView: null
|
||||
isOpen: notificationModal.notificationModalOpen
|
||||
onClose: function () {
|
||||
notificationModal.hide()
|
||||
}
|
||||
}
|
||||
|
||||
property bool notificationModalOpen: false
|
||||
property var notificationListRef: null
|
||||
|
||||
function show() {
|
||||
notificationModalOpen = true
|
||||
NotificationService.onOverlayOpen()
|
||||
open()
|
||||
modalKeyboardController.reset()
|
||||
|
||||
if (modalKeyboardController && notificationListRef) {
|
||||
modalKeyboardController.listView = notificationListRef
|
||||
modalKeyboardController.rebuildFlatNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
notificationModalOpen = false
|
||||
NotificationService.onOverlayClose()
|
||||
close()
|
||||
modalKeyboardController.reset()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible)
|
||||
hide()
|
||||
else
|
||||
show()
|
||||
onClose: () => notificationModal.hide()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
notificationModal.show()
|
||||
return "NOTIFICATION_MODAL_OPEN_SUCCESS"
|
||||
function open(): string {
|
||||
notificationModal.show();
|
||||
return "NOTIFICATION_MODAL_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close() {
|
||||
notificationModal.hide()
|
||||
return "NOTIFICATION_MODAL_CLOSE_SUCCESS"
|
||||
function close(): string {
|
||||
notificationModal.hide();
|
||||
return "NOTIFICATION_MODAL_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
notificationModal.toggle()
|
||||
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS"
|
||||
function toggle(): string {
|
||||
notificationModal.toggle();
|
||||
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "notifications"
|
||||
}
|
||||
|
||||
property Component notificationContent: Component {
|
||||
content: Component {
|
||||
Item {
|
||||
id: notificationKeyHandler
|
||||
|
||||
@@ -98,11 +103,13 @@ DankModal {
|
||||
|
||||
NotificationHeader {
|
||||
id: notificationHeader
|
||||
|
||||
keyboardController: modalKeyboardController
|
||||
}
|
||||
|
||||
NotificationSettings {
|
||||
id: notificationSettings
|
||||
|
||||
expanded: notificationHeader.showSettings
|
||||
}
|
||||
|
||||
@@ -112,7 +119,6 @@ DankModal {
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
keyboardController: modalKeyboardController
|
||||
|
||||
Component.onCompleted: {
|
||||
notificationModal.notificationListRef = notificationList
|
||||
if (modalKeyboardController) {
|
||||
@@ -121,18 +127,21 @@ DankModal {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
NotificationKeyboardHints {
|
||||
id: keyboardHints
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
showHints: modalKeyboardController.showKeyboardHints
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
content: notificationContent
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -10,66 +8,86 @@ DankModal {
|
||||
id: root
|
||||
|
||||
property int selectedIndex: 0
|
||||
property int optionCount: 4
|
||||
property int optionCount: SessionService.hibernateSupported ? 5 : 4
|
||||
|
||||
signal powerActionRequested(string action, string title, string message)
|
||||
|
||||
function selectOption() {
|
||||
close()
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
root.powerActionRequested("logout", "Log Out", "Are you sure you want to log out?")
|
||||
break
|
||||
case 1:
|
||||
root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend the system?")
|
||||
break
|
||||
case 2:
|
||||
root.powerActionRequested("reboot", "Reboot", "Are you sure you want to reboot the system?")
|
||||
break
|
||||
case 3:
|
||||
root.powerActionRequested("poweroff", "Power Off", "Are you sure you want to power off the system?")
|
||||
break
|
||||
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: 300
|
||||
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
|
||||
enableShadow: true
|
||||
onBackgroundClicked: {
|
||||
close()
|
||||
onBackgroundClicked: () => {
|
||||
return close();
|
||||
}
|
||||
onOpened: {
|
||||
selectedIndex = 0
|
||||
modalFocusScope.forceActiveFocus()
|
||||
onOpened: () => {
|
||||
selectedIndex = 0;
|
||||
modalFocusScope.forceActiveFocus();
|
||||
}
|
||||
modalFocusScope.Keys.onPressed: function(event) {
|
||||
modalFocusScope.Keys.onPressed: (event) => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Up:
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
||||
event.accepted = true
|
||||
break
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Down:
|
||||
selectedIndex = (selectedIndex + 1) % optionCount
|
||||
event.accepted = true
|
||||
break
|
||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Tab:
|
||||
selectedIndex = (selectedIndex + 1) % optionCount
|
||||
event.accepted = true
|
||||
break
|
||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
selectOption()
|
||||
event.accepted = true
|
||||
break
|
||||
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
|
||||
@@ -94,9 +112,8 @@ DankModal {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
onClicked: {
|
||||
close()
|
||||
onClicked: () => {
|
||||
return close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,12 +128,13 @@ DankModal {
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (selectedIndex === 0)
|
||||
if (selectedIndex === 0) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
else if (logoutArea.containsMouse)
|
||||
} else if (logoutArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
else
|
||||
} 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
|
||||
@@ -150,9 +168,9 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedIndex = 0
|
||||
selectOption()
|
||||
onClicked: () => {
|
||||
selectedIndex = 0;
|
||||
selectOption("logout");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,12 +181,13 @@ DankModal {
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (selectedIndex === 1)
|
||||
if (selectedIndex === 1) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
else if (suspendArea.containsMouse)
|
||||
} else if (suspendArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
else
|
||||
} 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
|
||||
@@ -202,9 +221,9 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedIndex = 1
|
||||
selectOption()
|
||||
onClicked: () => {
|
||||
selectedIndex = 1;
|
||||
selectOption("suspend");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,15 +234,71 @@ DankModal {
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (selectedIndex === 2)
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||
else if (rebootArea.containsMouse)
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08);
|
||||
else
|
||||
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.warning : "transparent"
|
||||
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
|
||||
@@ -254,9 +329,9 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedIndex = 2
|
||||
selectOption()
|
||||
onClicked: () => {
|
||||
selectedIndex = SessionService.hibernateSupported ? 3 : 2;
|
||||
selectOption("reboot");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,15 +342,17 @@ DankModal {
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (selectedIndex === 3)
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
else if (powerOffArea.containsMouse)
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
|
||||
else
|
||||
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 === 3 ? Theme.error : "transparent"
|
||||
border.width: selectedIndex === 3 ? 1 : 0
|
||||
border.color: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
@@ -306,9 +383,9 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedIndex = 3
|
||||
selectOption()
|
||||
onClicked: () => {
|
||||
selectedIndex = SessionService.hibernateSupported ? 4 : 3;
|
||||
selectOption("poweroff");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,14 +397,6 @@ DankModal {
|
||||
height: Theme.spacingS
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↑↓ Navigate • Tab Cycle • Enter Select • Esc Close"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -19,28 +14,31 @@ DankModal {
|
||||
|
||||
function show() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available")
|
||||
return
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
return ;
|
||||
}
|
||||
open()
|
||||
UserInfoService.getUptime()
|
||||
open();
|
||||
UserInfoService.getUptime();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
if (processContextMenu.visible)
|
||||
processContextMenu.close()
|
||||
close();
|
||||
if (processContextMenu.visible) {
|
||||
processContextMenu.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available")
|
||||
return
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
return ;
|
||||
}
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
if (shouldBeVisible)
|
||||
hide()
|
||||
else
|
||||
show()
|
||||
}
|
||||
|
||||
width: 900
|
||||
@@ -49,7 +47,9 @@ DankModal {
|
||||
backgroundColor: Theme.popupBackground()
|
||||
cornerRadius: Theme.cornerRadius
|
||||
enableShadow: true
|
||||
onBackgroundClicked: hide()
|
||||
onBackgroundClicked: () => {
|
||||
return hide();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: processesTabComponent
|
||||
@@ -57,18 +57,23 @@ DankModal {
|
||||
ProcessesTab {
|
||||
contextMenu: processContextMenu
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: performanceTabComponent
|
||||
|
||||
PerformanceTab {}
|
||||
PerformanceTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemTabComponent
|
||||
|
||||
SystemTab {}
|
||||
SystemTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ProcessContextMenu {
|
||||
@@ -79,19 +84,19 @@ DankModal {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onPressed: function (event) {
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
processListModal.hide()
|
||||
event.accepted = true
|
||||
processListModal.hide();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_1) {
|
||||
currentTab = 0
|
||||
event.accepted = true
|
||||
currentTab = 0;
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_2) {
|
||||
currentTab = 1
|
||||
event.accepted = true
|
||||
currentTab = 1;
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_3) {
|
||||
currentTab = 2
|
||||
event.accepted = true
|
||||
currentTab = 2;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +138,9 @@ DankModal {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -163,16 +170,18 @@ DankModal {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Theme.errorHover
|
||||
onClicked: processListModal.hide()
|
||||
onClicked: () => {
|
||||
return processListModal.hide();
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.surfaceSelected
|
||||
color: Theme.surfaceContainerHigh
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
@@ -199,20 +208,11 @@ DankModal {
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "list_alt"
|
||||
case 1:
|
||||
return "analytics"
|
||||
case 2:
|
||||
return "settings"
|
||||
default:
|
||||
return "tab"
|
||||
}
|
||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||
return tabIcons[index] || "tab";
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab
|
||||
=== index ? Theme.primary : Theme.surfaceText
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -220,15 +220,16 @@ DankModal {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: currentTab
|
||||
=== index ? Theme.primary : Theme.surfaceText
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -1
|
||||
|
||||
@@ -236,8 +237,11 @@ DankModal {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -246,8 +250,8 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
currentTab = index
|
||||
onClicked: () => {
|
||||
currentTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,23 +259,29 @@ DankModal {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceLight
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
@@ -290,7 +300,9 @@ DankModal {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -308,7 +320,9 @@ DankModal {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -326,10 +340,17 @@ DankModal {
|
||||
duration: Theme.mediumDuration
|
||||
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,606 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: settingsModal
|
||||
|
||||
property Component settingsContent
|
||||
|
||||
signal closingModal
|
||||
|
||||
function show() {
|
||||
open()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible)
|
||||
hide()
|
||||
else
|
||||
show()
|
||||
}
|
||||
|
||||
objectName: "settingsModal"
|
||||
width: 800
|
||||
height: 750
|
||||
visible: false
|
||||
onBackgroundClicked: hide()
|
||||
content: settingsContent
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
settingsModal.show()
|
||||
return "SETTINGS_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close() {
|
||||
settingsModal.hide()
|
||||
return "SETTINGS_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
settingsModal.toggle()
|
||||
return "SETTINGS_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "settings"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function browse(type: string) {
|
||||
if (type === "wallpaper") {
|
||||
wallpaperBrowser.allowStacking = false
|
||||
wallpaperBrowser.open()
|
||||
} else if (type === "profile") {
|
||||
profileBrowser.allowStacking = false
|
||||
profileBrowser.open()
|
||||
}
|
||||
}
|
||||
|
||||
target: "file"
|
||||
}
|
||||
|
||||
settingsContent: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 35
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Settings"
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Theme.errorHover
|
||||
onClicked: settingsModal.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: parent.height - 35
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
id: sidebarContainer
|
||||
|
||||
property int currentIndex: 0
|
||||
|
||||
width: 270
|
||||
height: parent.height
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
anchors.topMargin: Theme.spacingM + 2
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 110
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
id: profileImageContainer
|
||||
width: 80
|
||||
height: 80
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
property bool hasImage: profileImageSource.status
|
||||
=== Image.Ready
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
visible: parent.hasImage
|
||||
}
|
||||
|
||||
Image {
|
||||
id: profileImageSource
|
||||
source: {
|
||||
if (PortalService.profileImage === "")
|
||||
return ""
|
||||
if (PortalService.profileImage.startsWith(
|
||||
"/"))
|
||||
return "file://" + PortalService.profileImage
|
||||
return PortalService.profileImage
|
||||
}
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
mipmap: true
|
||||
cache: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
source: profileImageSource
|
||||
maskEnabled: true
|
||||
maskSource: profileCircularMask
|
||||
visible: profileImageContainer.hasImage
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: profileCircularMask
|
||||
width: 70
|
||||
height: 70
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: Theme.primary
|
||||
visible: !parent.hasImage
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "person"
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primaryText
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: Qt.rgba(0, 0, 0, 0.7)
|
||||
visible: profileMouseArea.containsMouse
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: Qt.rgba(255, 255,
|
||||
255, 0.9)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "edit"
|
||||
size: 16
|
||||
color: "black"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
settingsModal.allowFocusOverride = true
|
||||
settingsModal.shouldHaveFocus = false
|
||||
profileBrowser.open(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: Qt.rgba(255, 255,
|
||||
255, 0.9)
|
||||
visible: profileImageContainer.hasImage
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: 16
|
||||
color: "black"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
PortalService.setProfileImage(
|
||||
"")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: profileMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: 120
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: UserInfoService.fullName
|
||||
|| "User"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.distribution
|
||||
|| "Linux"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.spacingL
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sidebarRepeater
|
||||
|
||||
model: [{
|
||||
"text": "Personalization",
|
||||
"icon": "person"
|
||||
}, {
|
||||
"text": "Time & Date",
|
||||
"icon": "schedule"
|
||||
}, {
|
||||
"text": "Weather",
|
||||
"icon": "cloud"
|
||||
}, {
|
||||
"text": "Top Bar",
|
||||
"icon": "toolbar"
|
||||
}, {
|
||||
"text": "Widgets",
|
||||
"icon": "widgets"
|
||||
}, {
|
||||
"text": "Dock",
|
||||
"icon": "dock_to_bottom"
|
||||
}, {
|
||||
"text": "Displays",
|
||||
"icon": "monitor"
|
||||
}, {
|
||||
"text": "Recent Apps",
|
||||
"icon": "history"
|
||||
}, {
|
||||
"text": "Theme & Colors",
|
||||
"icon": "palette"
|
||||
}, {
|
||||
"text": "About",
|
||||
"icon": "info"
|
||||
}]
|
||||
|
||||
Rectangle {
|
||||
property bool isActive: sidebarContainer.currentIndex === index
|
||||
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.surfaceContainerHigh : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || ""
|
||||
size: Theme.iconSize - 2
|
||||
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
|
||||
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
sidebarContainer.currentIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - sidebarContainer.width
|
||||
height: parent.height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
anchors.topMargin: 0
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: personalizationLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 0
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Component {
|
||||
PersonalizationTab {
|
||||
parentModal: settingsModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: timeLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 1
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: TimeTab {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: weatherLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 2
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: WeatherTab {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: topBarLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 3
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: TopBarTab {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: widgetsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 4
|
||||
visible: active
|
||||
asynchronous: true
|
||||
sourceComponent: WidgetTweaksTab {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dockLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 5
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Component {
|
||||
DockTab {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: displaysLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 6
|
||||
visible: active
|
||||
asynchronous: true
|
||||
sourceComponent: DisplaysTab {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: recentAppsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 7
|
||||
visible: active
|
||||
asynchronous: true
|
||||
sourceComponent: RecentAppsTab {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: themeColorsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 8
|
||||
visible: active
|
||||
asynchronous: true
|
||||
sourceComponent: ThemeColorsTab {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: aboutLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: sidebarContainer.currentIndex === 9
|
||||
visible: active
|
||||
asynchronous: true
|
||||
sourceComponent: AboutTab {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: profileBrowser
|
||||
|
||||
allowStacking: true
|
||||
browserTitle: "Select Profile Image"
|
||||
browserIcon: "person"
|
||||
browserType: "profile"
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||
onFileSelected: path => {
|
||||
PortalService.setProfileImage(path)
|
||||
close()
|
||||
}
|
||||
onDialogClosed: {
|
||||
if (settingsModal) {
|
||||
settingsModal.allowFocusOverride = false
|
||||
settingsModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return settingsModal.shouldBeVisible
|
||||
})
|
||||
}
|
||||
allowStacking = true
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: wallpaperBrowser
|
||||
|
||||
allowStacking: true
|
||||
browserTitle: "Select Wallpaper"
|
||||
browserIcon: "wallpaper"
|
||||
browserType: "wallpaper"
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||
onFileSelected: path => {
|
||||
SessionData.setWallpaper(path)
|
||||
close()
|
||||
}
|
||||
onDialogClosed: {
|
||||
allowStacking = true
|
||||
}
|
||||
}
|
||||
}
|
||||
221
Modals/Spotlight/SpotlightContent.qml
Normal file
221
Modals/Spotlight/SpotlightContent.qml
Normal file
@@ -0,0 +1,221 @@
|
||||
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.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: categorySelector.height + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0
|
||||
|
||||
CategorySelector {
|
||||
id: categorySelector
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
categories: appLauncher.categories
|
||||
selectedCategory: appLauncher.selectedCategory
|
||||
compact: false
|
||||
onCategorySelected: category => {
|
||||
appLauncher.setCategory(category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
leftPadding: Theme.spacingS
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width - 80 - Theme.spacingL
|
||||
height: 56
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.surfaceContainerHigh
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: parentModal ? parentModal.spotlightOpen : true
|
||||
placeholderText: ""
|
||||
ignoreLeftRightKeys: 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"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "view_list"
|
||||
size: 18
|
||||
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "grid_view"
|
||||
size: 18
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("grid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightResults {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
318
Modals/Spotlight/SpotlightResults.qml
Normal file
318
Modals/Spotlight/SpotlightResults.qml
Normal file
@@ -0,0 +1,318 @@
|
||||
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: "transparent"
|
||||
|
||||
DankListView {
|
||||
id: resultsList
|
||||
|
||||
property int itemHeight: 60
|
||||
property int iconSize: 40
|
||||
property bool showDescription: true
|
||||
property int itemSpacing: Theme.spacingS
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
const itemY = index * (itemHeight + itemSpacing)
|
||||
const itemBottom = itemY + itemHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appLauncher && appLauncher.viewMode === "list"
|
||||
model: appLauncher ? appLauncher.model : null
|
||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||
clip: true
|
||||
spacing: itemSpacing
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: (index, modelData) => {
|
||||
if (appLauncher)
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||
if (contextMenu)
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: () => {
|
||||
if (appLauncher)
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: resultsList.itemHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
width: resultsList.iconSize
|
||||
height: resultsList.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
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 : Theme.surfaceContainerHigh
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio))
|
||||
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
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,914 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: spotlightModal
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property Component spotlightContent
|
||||
|
||||
function show() {
|
||||
spotlightOpen = true
|
||||
open()
|
||||
if (contentLoader.item && contentLoader.item.appLauncher)
|
||||
contentLoader.item.appLauncher.searchQuery = ""
|
||||
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item && contentLoader.item.searchField)
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
spotlightOpen = false
|
||||
close()
|
||||
if (contentLoader.item && contentLoader.item.appLauncher) {
|
||||
contentLoader.item.appLauncher.searchQuery = ""
|
||||
contentLoader.item.appLauncher.selectedIndex = 0
|
||||
contentLoader.item.appLauncher.setCategory("All")
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (spotlightOpen)
|
||||
hide()
|
||||
else
|
||||
show()
|
||||
}
|
||||
|
||||
shouldBeVisible: spotlightOpen
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== spotlightModal && !allowStacking
|
||||
&& spotlightOpen) {
|
||||
spotlightOpen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
width: 550
|
||||
height: 600
|
||||
backgroundColor: Theme.popupBackground()
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
enableShadow: true
|
||||
onVisibleChanged: {
|
||||
if (visible && !spotlightOpen)
|
||||
show()
|
||||
|
||||
if (visible && contentLoader.item)
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item.searchField)
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
onBackgroundClicked: {
|
||||
hide()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
|
||||
}
|
||||
content: spotlightContent
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
spotlightModal.show()
|
||||
return "SPOTLIGHT_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close() {
|
||||
spotlightModal.hide()
|
||||
return "SPOTLIGHT_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
spotlightModal.toggle()
|
||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "spotlight"
|
||||
}
|
||||
|
||||
spotlightContent: Component {
|
||||
Item {
|
||||
id: spotlightKeyHandler
|
||||
|
||||
property alias appLauncher: appLauncher
|
||||
property alias searchField: searchField
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onPressed: function (event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
hide()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
appLauncher.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
appLauncher.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Right
|
||||
&& appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Left
|
||||
&& appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter) {
|
||||
appLauncher.launchSelected()
|
||||
event.accepted = true
|
||||
} else if (!searchField.activeFocus && event.text
|
||||
&& event.text.length > 0 && event.text.match(
|
||||
/[a-zA-Z0-9\\s]/)) {
|
||||
searchField.forceActiveFocus()
|
||||
searchField.insertText(event.text)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
AppLauncher {
|
||||
id: appLauncher
|
||||
|
||||
viewMode: SettingsData.spotlightModalViewMode
|
||||
gridColumns: 4
|
||||
onAppLaunched: hide()
|
||||
onViewModeSelected: function (mode) {
|
||||
SettingsData.setSpotlightModalViewMode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: categorySelector.height + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariantAlpha
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
visible: appLauncher.categories.length > 1
|
||||
|| appLauncher.model.count > 0
|
||||
|
||||
CategorySelector {
|
||||
id: categorySelector
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
categories: appLauncher.categories
|
||||
selectedCategory: appLauncher.selectedCategory
|
||||
compact: false
|
||||
onCategorySelected: category => {
|
||||
return appLauncher.setCategory(
|
||||
category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width - 80
|
||||
- Theme.spacingM // Leave space for view toggle buttons
|
||||
height: 56
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
Theme.getContentBackgroundAlpha(
|
||||
) * 0.7)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: spotlightOpen
|
||||
placeholderText: ""
|
||||
ignoreLeftRightKeys: true
|
||||
keyForwardTargets: [spotlightKeyHandler]
|
||||
text: appLauncher.searchQuery
|
||||
onTextEdited: {
|
||||
appLauncher.searchQuery = text
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
hide()
|
||||
event.accepted = true
|
||||
} else if ((event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)
|
||||
&& text.length > 0) {
|
||||
if (appLauncher.keyboardNavigationActive
|
||||
&& appLauncher.model.count > 0)
|
||||
appLauncher.launchSelected()
|
||||
else if (appLauncher.model.count > 0)
|
||||
appLauncher.launchApp(
|
||||
appLauncher.model.get(0))
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: appLauncher.model.count > 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
border.color: appLauncher.viewMode
|
||||
=== "list" ? Theme.primarySelected : "transparent"
|
||||
border.width: 1
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "view_list"
|
||||
size: 18
|
||||
color: appLauncher.viewMode
|
||||
=== "list" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
appLauncher.setViewMode("list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
border.color: appLauncher.viewMode
|
||||
=== "grid" ? Theme.primarySelected : "transparent"
|
||||
border.width: 1
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "grid_view"
|
||||
size: 18
|
||||
color: appLauncher.viewMode
|
||||
=== "grid" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
appLauncher.setViewMode("grid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: resultsContainer
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceLight
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
DankListView {
|
||||
id: resultsList
|
||||
|
||||
property int itemHeight: 60
|
||||
property int iconSize: 40
|
||||
property bool showDescription: true
|
||||
property int itemSpacing: Theme.spacingS
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
var itemY = index * (itemHeight + itemSpacing)
|
||||
var itemBottom = itemY + itemHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appLauncher.viewMode === "list"
|
||||
model: appLauncher.model
|
||||
currentIndex: appLauncher.selectedIndex
|
||||
clip: true
|
||||
spacing: itemSpacing
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: function (index, modelData) {
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: {
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOn
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: resultsList.itemHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
||||
border.width: ListView.isCurrentItem ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
width: resultsList.iconSize
|
||||
height: resultsList.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
IconImage {
|
||||
id: listIconImg
|
||||
|
||||
anchors.fill: parent
|
||||
source: Quickshell.iconPath(model.icon, true)
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !listIconImg.visible
|
||||
color: Theme.surfaceLight
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: (model.name
|
||||
&& model.name.length
|
||||
> 0) ? model.name.charAt(
|
||||
0).toUpperCase(
|
||||
) : "A"
|
||||
font.pixelSize: resultsList.iconSize * 0.4
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - resultsList.iconSize - Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: model.name || ""
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: model.comment || "Application"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
visible: resultsList.showDescription
|
||||
&& model.comment
|
||||
&& model.comment.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
z: 10
|
||||
onEntered: {
|
||||
if (resultsList.hoverUpdatesSelection
|
||||
&& !resultsList.keyboardNavigationActive)
|
||||
resultsList.currentIndex = index
|
||||
}
|
||||
onPositionChanged: {
|
||||
resultsList.keyboardNavigationReset()
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
resultsList.itemClicked(
|
||||
index, model)
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
var modalPos = mapToItem(
|
||||
spotlightKeyHandler,
|
||||
mouse.x, mouse.y)
|
||||
resultsList.itemRightClicked(
|
||||
index, model,
|
||||
modalPos.x, modalPos.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: resultsGrid
|
||||
|
||||
property int currentIndex: appLauncher.selectedIndex
|
||||
property int columns: 4
|
||||
property bool adaptiveColumns: false
|
||||
property int minCellWidth: 120
|
||||
property int maxCellWidth: 160
|
||||
property int cellPadding: 8
|
||||
property real iconSizeRatio: 0.55
|
||||
property int maxIconSize: 48
|
||||
property int minIconSize: 32
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||
property int baseCellWidth: adaptiveColumns ? Math.max(
|
||||
minCellWidth,
|
||||
Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||
property int baseCellHeight: baseCellWidth + 20
|
||||
property int actualColumns: adaptiveColumns ? Math.floor(
|
||||
width
|
||||
/ cellWidth) : columns
|
||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
var itemY = Math.floor(
|
||||
index / actualColumns) * cellHeight
|
||||
var itemBottom = itemY + cellHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appLauncher.viewMode === "grid"
|
||||
model: appLauncher.model
|
||||
clip: true
|
||||
cellWidth: baseCellWidth
|
||||
cellHeight: baseCellHeight
|
||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||
rightMargin: leftMargin
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: function (index, modelData) {
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: {
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: resultsGrid.cellWidth - resultsGrid.cellPadding
|
||||
height: resultsGrid.cellHeight - resultsGrid.cellPadding
|
||||
radius: Theme.cornerRadius
|
||||
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||
border.color: resultsGrid.currentIndex
|
||||
=== index ? Theme.primarySelected : Theme.outlineMedium
|
||||
border.width: resultsGrid.currentIndex === index ? 2 : 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
property int iconSize: Math.min(
|
||||
resultsGrid.maxIconSize,
|
||||
Math.max(
|
||||
resultsGrid.minIconSize,
|
||||
resultsGrid.cellWidth
|
||||
* resultsGrid.iconSizeRatio))
|
||||
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
IconImage {
|
||||
id: gridIconImg
|
||||
|
||||
anchors.fill: parent
|
||||
source: Quickshell.iconPath(model.icon, true)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !gridIconImg.visible
|
||||
color: Theme.surfaceLight
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: (model.name
|
||||
&& model.name.length
|
||||
> 0) ? model.name.charAt(
|
||||
0).toUpperCase(
|
||||
) : "A"
|
||||
font.pixelSize: Math.min(
|
||||
28,
|
||||
parent.width * 0.5)
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: resultsGrid.cellWidth - 12
|
||||
text: model.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
z: 10
|
||||
onEntered: {
|
||||
if (resultsGrid.hoverUpdatesSelection
|
||||
&& !resultsGrid.keyboardNavigationActive)
|
||||
resultsGrid.currentIndex = index
|
||||
}
|
||||
onPositionChanged: {
|
||||
resultsGrid.keyboardNavigationReset()
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
resultsGrid.itemClicked(
|
||||
index, model)
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
var modalPos = mapToItem(
|
||||
spotlightKeyHandler,
|
||||
mouse.x, mouse.y)
|
||||
resultsGrid.itemRightClicked(
|
||||
index, model,
|
||||
modalPos.x, modalPos.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contextMenu
|
||||
|
||||
property var currentApp: null
|
||||
property bool menuVisible: false
|
||||
|
||||
function show(x, y, app) {
|
||||
currentApp = app
|
||||
|
||||
const menuWidth = 180
|
||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
let finalX = x + 8
|
||||
let finalY = y + 8
|
||||
|
||||
if (finalX + menuWidth > spotlightKeyHandler.width) {
|
||||
finalX = x - menuWidth - 8
|
||||
}
|
||||
|
||||
if (finalY + menuHeight > spotlightKeyHandler.height) {
|
||||
finalY = y - menuHeight - 8
|
||||
}
|
||||
|
||||
finalX = Math.max(
|
||||
8, Math.min(
|
||||
finalX,
|
||||
spotlightKeyHandler.width - menuWidth - 8))
|
||||
finalY = Math.max(
|
||||
8, Math.min(
|
||||
finalY,
|
||||
spotlightKeyHandler.height - menuHeight - 8))
|
||||
|
||||
contextMenu.x = finalX
|
||||
contextMenu.y = finalY
|
||||
contextMenu.visible = true
|
||||
contextMenu.menuVisible = true
|
||||
}
|
||||
|
||||
function close() {
|
||||
contextMenu.menuVisible = false
|
||||
Qt.callLater(() => {
|
||||
contextMenu.visible = false
|
||||
})
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: 180
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.popupBackground()
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
z: 1000
|
||||
opacity: menuVisible ? 1 : 0
|
||||
scale: menuVisible ? 1 : 0.85
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: pinMouseArea.containsMouse ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (!contextMenu.currentApp
|
||||
|| !contextMenu.currentApp.desktopEntry)
|
||||
return "push_pin"
|
||||
|
||||
var appId = contextMenu.currentApp.desktopEntry.id
|
||||
|| contextMenu.currentApp.desktopEntry.execString
|
||||
|| ""
|
||||
return SessionData.isPinnedApp(
|
||||
appId) ? "keep_off" : "push_pin"
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!contextMenu.currentApp
|
||||
|| !contextMenu.currentApp.desktopEntry)
|
||||
return "Pin to Dock"
|
||||
|
||||
var appId = contextMenu.currentApp.desktopEntry.id
|
||||
|| contextMenu.currentApp.desktopEntry.execString
|
||||
|| ""
|
||||
return SessionData.isPinnedApp(
|
||||
appId) ? "Unpin from Dock" : "Pin to Dock"
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!contextMenu.currentApp
|
||||
|| !contextMenu.currentApp.desktopEntry)
|
||||
return
|
||||
|
||||
var appId = contextMenu.currentApp.desktopEntry.id
|
||||
|| contextMenu.currentApp.desktopEntry.execString
|
||||
|| ""
|
||||
if (SessionData.isPinnedApp(appId))
|
||||
SessionData.removePinnedApp(appId)
|
||||
else
|
||||
SessionData.addPinnedApp(appId)
|
||||
contextMenu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: launchMouseArea.containsMouse ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "launch"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Launch"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: launchMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (contextMenu.currentApp)
|
||||
appLauncher.launchApp(
|
||||
contextMenu.currentApp)
|
||||
|
||||
contextMenu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: contextMenu.visible
|
||||
z: 999
|
||||
onClicked: {
|
||||
contextMenu.close()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: contextMenu.x
|
||||
y: contextMenu.y
|
||||
width: contextMenu.width
|
||||
height: contextMenu.height
|
||||
onClicked: {
|
||||
|
||||
// Prevent closing when clicking on the menu itself
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -14,58 +14,56 @@ DankModal {
|
||||
wifiPasswordSSID = ssid
|
||||
wifiPasswordInput = ""
|
||||
open()
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item && contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.passwordInput)
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
width: 420
|
||||
height: 230
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible)
|
||||
wifiPasswordInput = ""
|
||||
}
|
||||
onShouldBeVisibleChanged: () => {
|
||||
if (!shouldBeVisible)
|
||||
wifiPasswordInput = ""
|
||||
}
|
||||
onOpened: {
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item && contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
onBackgroundClicked: {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.passwordInput)
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NetworkService
|
||||
|
||||
function onPasswordDialogShouldReopenChanged() {
|
||||
if (NetworkService.passwordDialogShouldReopen
|
||||
&& NetworkService.connectingSSID !== "") {
|
||||
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
|
||||
wifiPasswordSSID = NetworkService.connectingSSID
|
||||
wifiPasswordInput = ""
|
||||
open()
|
||||
NetworkService.passwordDialogShouldReopen = false
|
||||
}
|
||||
}
|
||||
|
||||
target: NetworkService
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: wifiContent
|
||||
|
||||
property alias passwordInput: passwordInput
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onEscapePressed: function (event) {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
event.accepted = true
|
||||
}
|
||||
Keys.onEscapePressed: event => {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
@@ -87,7 +85,7 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Enter password for \"" + wifiPasswordSSID + "\""
|
||||
text: `Enter password for "${wifiPasswordSSID}"`
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
@@ -99,11 +97,10 @@ DankModal {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Theme.errorHover
|
||||
onClicked: {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
}
|
||||
onClicked: () => {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +114,9 @@ DankModal {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
onClicked: () => {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
@@ -134,40 +131,37 @@ DankModal {
|
||||
backgroundColor: "transparent"
|
||||
focus: true
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: {
|
||||
wifiPasswordInput = text
|
||||
}
|
||||
onAccepted: {
|
||||
NetworkService.connectToWifiWithPassword(
|
||||
wifiPasswordSSID, passwordInput.text)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
passwordInput.text = ""
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible) {
|
||||
focusDelayTimer.start()
|
||||
}
|
||||
}
|
||||
onTextEdited: () => {
|
||||
wifiPasswordInput = text
|
||||
}
|
||||
onAccepted: () => {
|
||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
passwordInput.text = ""
|
||||
}
|
||||
Component.onCompleted: () => {
|
||||
if (root.shouldBeVisible)
|
||||
focusDelayTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: focusDelayTimer
|
||||
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (root.shouldBeVisible) {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
onTriggered: () => {
|
||||
if (root.shouldBeVisible)
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
if (root.shouldBeVisible)
|
||||
focusDelayTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,9 +194,9 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||
}
|
||||
onClicked: () => {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,9 +218,7 @@ DankModal {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(
|
||||
70,
|
||||
cancelText.contentWidth + Theme.spacingM * 2)
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
@@ -249,22 +241,18 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
}
|
||||
onClicked: () => {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(
|
||||
80,
|
||||
connectText.contentWidth + Theme.spacingM * 2)
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(
|
||||
Theme.primary,
|
||||
1.1) : Theme.primary
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: passwordInput.text.length > 0
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
@@ -285,14 +273,12 @@ DankModal {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
NetworkService.connectToWifiWithPassword(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
passwordInput.text = ""
|
||||
}
|
||||
onClicked: () => {
|
||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
passwordInput.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
@@ -16,16 +15,11 @@ DankPopout {
|
||||
property string triggerSection: "left"
|
||||
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() {
|
||||
open()
|
||||
appLauncher.searchQuery = ""
|
||||
appLauncher.selectedIndex = 0
|
||||
appLauncher.setCategory("All")
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
}
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
@@ -39,18 +33,23 @@ DankPopout {
|
||||
popupWidth: 520
|
||||
popupHeight: 600
|
||||
triggerX: Theme.spacingL
|
||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
||||
triggerY: Math.max(26 + SettingsData.topBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.topBarInnerPadding)) + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance
|
||||
triggerWidth: 40
|
||||
positioning: "center"
|
||||
WlrLayershell.namespace: "quickshell-launcher"
|
||||
screen: triggerScreen
|
||||
|
||||
onOpened: {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
appLauncher.searchQuery = ""
|
||||
appLauncher.selectedIndex = 0
|
||||
appLauncher.setCategory("All")
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.text = ""
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
AppLauncher {
|
||||
@@ -58,7 +57,7 @@ DankPopout {
|
||||
|
||||
viewMode: SettingsData.appLauncherViewMode
|
||||
gridColumns: 4
|
||||
onAppLaunched: appDrawerPopout.hide()
|
||||
onAppLaunched: appDrawerPopout.close()
|
||||
onViewModeSelected: function (mode) {
|
||||
SettingsData.setAppLauncherViewMode(mode)
|
||||
}
|
||||
@@ -75,34 +74,30 @@ DankPopout {
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
color: "transparent"
|
||||
radius: parent.radius + 3
|
||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
||||
border.width: 1
|
||||
z: -3
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -2
|
||||
color: "transparent"
|
||||
radius: parent.radius + 2
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
border.width: 1
|
||||
z: -2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
// Multi-layer border effect
|
||||
Repeater {
|
||||
model: [{
|
||||
"margin": -3,
|
||||
"color": Qt.rgba(0, 0, 0, 0.05),
|
||||
"z": -3
|
||||
}, {
|
||||
"margin": -2,
|
||||
"color": Qt.rgba(0, 0, 0, 0.08),
|
||||
"z": -2
|
||||
}, {
|
||||
"margin": 0,
|
||||
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
|
||||
"z": -1
|
||||
}]
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: modelData.margin
|
||||
color: "transparent"
|
||||
radius: parent.radius + Math.abs(modelData.margin)
|
||||
border.color: modelData.color
|
||||
border.width: 0
|
||||
z: modelData.z
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -110,31 +105,30 @@ DankPopout {
|
||||
|
||||
anchors.fill: parent
|
||||
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) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
appDrawerPopout.close()
|
||||
if (keyMappings[event.key]) {
|
||||
keyMappings[event.key]()
|
||||
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]/)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!searchField.activeFocus && event.text && /[a-zA-Z0-9\s]/.test(event.text)) {
|
||||
searchField.forceActiveFocus()
|
||||
searchField.insertText(event.text)
|
||||
event.accepted = true
|
||||
@@ -142,15 +136,16 @@ DankPopout {
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
height: parent.height - Theme.spacingL * 2
|
||||
x: Theme.spacingL
|
||||
y: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: parent.height - Theme.spacingS * 2
|
||||
x: Theme.spacingS
|
||||
y: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
leftPadding: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -176,18 +171,12 @@ DankPopout {
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: 52
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
Theme.getContentBackgroundAlpha(
|
||||
) * 0.7)
|
||||
normalBorderColor: Qt.rgba(Theme.outline.r,
|
||||
Theme.outline.g,
|
||||
Theme.outline.b, 0.3)
|
||||
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
|
||||
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
@@ -205,28 +194,34 @@ DankPopout {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
appDrawerPopout.close()
|
||||
event.accepted = true
|
||||
} else if ((event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)
|
||||
&& text.length > 0) {
|
||||
if (appLauncher.keyboardNavigationActive
|
||||
&& appLauncher.model.count > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
|
||||
const hasText = text.length > 0
|
||||
|
||||
if (isEnterKey && hasText) {
|
||||
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
|
||||
appLauncher.launchSelected()
|
||||
} else if (appLauncher.model.count > 0) {
|
||||
var firstApp = appLauncher.model.get(0)
|
||||
appLauncher.launchApp(firstApp)
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (!appDrawerPopout.shouldBeVisible)
|
||||
searchField.clearFocus()
|
||||
if (!appDrawerPopout.shouldBeVisible) {
|
||||
searchField.focus = false
|
||||
}
|
||||
}
|
||||
|
||||
target: appDrawerPopout
|
||||
@@ -238,6 +233,8 @@ DankPopout {
|
||||
height: 40
|
||||
spacing: Theme.spacingM
|
||||
visible: searchField.text.length === 0
|
||||
leftPadding: Theme.spacingS
|
||||
topPadding: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: 200
|
||||
@@ -245,6 +242,9 @@ DankPopout {
|
||||
|
||||
DankDropdown {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
text: ""
|
||||
currentValue: appLauncher.selectedCategory
|
||||
options: appLauncher.categories
|
||||
@@ -256,7 +256,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 300
|
||||
width: parent.width - 310
|
||||
height: 1
|
||||
}
|
||||
|
||||
@@ -269,24 +269,8 @@ DankPopout {
|
||||
circular: false
|
||||
iconName: "view_list"
|
||||
iconSize: 20
|
||||
iconColor: appLauncher.viewMode
|
||||
=== "list" ? Theme.primary : Theme.surfaceText
|
||||
hoverColor: appLauncher.viewMode
|
||||
=== "list" ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
backgroundColor: appLauncher.viewMode
|
||||
=== "list" ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : "transparent"
|
||||
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
onClicked: {
|
||||
appLauncher.setViewMode("list")
|
||||
}
|
||||
@@ -297,24 +281,8 @@ DankPopout {
|
||||
circular: false
|
||||
iconName: "grid_view"
|
||||
iconSize: 20
|
||||
iconColor: appLauncher.viewMode
|
||||
=== "grid" ? Theme.primary : Theme.surfaceText
|
||||
hoverColor: appLauncher.viewMode
|
||||
=== "grid" ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
backgroundColor: appLauncher.viewMode
|
||||
=== "grid" ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : "transparent"
|
||||
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
onClicked: {
|
||||
appLauncher.setViewMode("grid")
|
||||
}
|
||||
@@ -325,18 +293,13 @@ DankPopout {
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: {
|
||||
let usedHeight = 40 + Theme.spacingL
|
||||
usedHeight += 52 + Theme.spacingL
|
||||
usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0)
|
||||
let usedHeight = 40 + Theme.spacingS
|
||||
usedHeight += 52 + Theme.spacingS
|
||||
usedHeight += (searchField.text.length === 0 ? 40 : 0)
|
||||
return parent.height - usedHeight
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b, 0.1)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.05)
|
||||
border.width: 1
|
||||
color: "transparent"
|
||||
|
||||
DankListView {
|
||||
id: appList
|
||||
@@ -365,7 +328,9 @@ DankPopout {
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: appLauncher.viewMode === "list"
|
||||
model: appLauncher.model
|
||||
currentIndex: appLauncher.selectedIndex
|
||||
@@ -391,21 +356,11 @@ DankPopout {
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOn
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: appList.itemHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
||||
border.width: ListView.isCurrentItem ? 2 : 1
|
||||
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
@@ -421,6 +376,7 @@ DankPopout {
|
||||
id: listIconImg
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
source: Quickshell.iconPath(model.icon, true)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
@@ -429,19 +385,18 @@ DankPopout {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
visible: !listIconImg.visible
|
||||
color: Theme.surfaceLight
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
border.color: Theme.primarySelected
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: (model.name
|
||||
&& model.name.length
|
||||
> 0) ? model.name.charAt(
|
||||
0).toUpperCase(
|
||||
) : "A"
|
||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
font.pixelSize: appList.iconSize * 0.4
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
@@ -469,9 +424,8 @@ DankPopout {
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
visible: appList.showDescription
|
||||
&& model.comment
|
||||
&& model.comment.length > 0
|
||||
maximumLineCount: 1
|
||||
visible: appList.showDescription && model.comment && model.comment.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,13 +434,15 @@ DankPopout {
|
||||
id: listMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
z: 10
|
||||
onEntered: {
|
||||
if (appList.hoverUpdatesSelection
|
||||
&& !appList.keyboardNavigationActive)
|
||||
if (appList.hoverUpdatesSelection && !appList.keyboardNavigationActive)
|
||||
appList.currentIndex = index
|
||||
}
|
||||
onPositionChanged: {
|
||||
@@ -494,16 +450,10 @@ DankPopout {
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
appList.itemClicked(
|
||||
index, model)
|
||||
appList.itemClicked(index, model)
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
var panelPos = mapToItem(
|
||||
contextMenu.parent,
|
||||
mouse.x, mouse.y)
|
||||
appList.itemRightClicked(
|
||||
index, model,
|
||||
panelPos.x,
|
||||
panelPos.y)
|
||||
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
|
||||
appList.itemRightClicked(index, model, panelPos.x, panelPos.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,8 +487,7 @@ DankPopout {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
var itemY = Math.floor(
|
||||
index / actualColumns) * cellHeight
|
||||
var itemY = Math.floor(index / actualColumns) * cellHeight
|
||||
var itemBottom = itemY + cellHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
@@ -547,14 +496,15 @@ DankPopout {
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: appLauncher.viewMode === "grid"
|
||||
model: appLauncher.model
|
||||
clip: true
|
||||
cellWidth: baseCellWidth
|
||||
cellHeight: baseCellHeight
|
||||
leftMargin: Math.max(Theme.spacingS,
|
||||
remainingSpace / 2)
|
||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||
rightMargin: leftMargin
|
||||
focus: true
|
||||
interactive: true
|
||||
@@ -576,33 +526,18 @@ DankPopout {
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: appGrid.cellWidth - appGrid.cellPadding
|
||||
height: appGrid.cellHeight - appGrid.cellPadding
|
||||
radius: Theme.cornerRadius
|
||||
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||
border.color: appGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
|
||||
border.width: appGrid.currentIndex === index ? 2 : 1
|
||||
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
property int iconSize: Math.min(
|
||||
appGrid.maxIconSize,
|
||||
Math.max(
|
||||
appGrid.minIconSize,
|
||||
appGrid.cellWidth
|
||||
* appGrid.iconSizeRatio))
|
||||
property int iconSize: Math.min(appGrid.maxIconSize, Math.max(appGrid.minIconSize, appGrid.cellWidth * appGrid.iconSizeRatio))
|
||||
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
@@ -612,6 +547,9 @@ DankPopout {
|
||||
id: gridIconImg
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
source: Quickshell.iconPath(model.icon, true)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
@@ -620,22 +558,19 @@ DankPopout {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: !gridIconImg.visible
|
||||
color: Theme.surfaceLight
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
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)
|
||||
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
|
||||
}
|
||||
@@ -660,13 +595,15 @@ DankPopout {
|
||||
id: gridMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
z: 10
|
||||
onEntered: {
|
||||
if (appGrid.hoverUpdatesSelection
|
||||
&& !appGrid.keyboardNavigationActive)
|
||||
if (appGrid.hoverUpdatesSelection && !appGrid.keyboardNavigationActive)
|
||||
appGrid.currentIndex = index
|
||||
}
|
||||
onPositionChanged: {
|
||||
@@ -674,16 +611,10 @@ DankPopout {
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
appGrid.itemClicked(
|
||||
index, model)
|
||||
appGrid.itemClicked(index, model)
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
var panelPos = mapToItem(
|
||||
contextMenu.parent,
|
||||
mouse.x, mouse.y)
|
||||
appGrid.itemRightClicked(
|
||||
index, model,
|
||||
panelPos.x,
|
||||
panelPos.y)
|
||||
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
|
||||
appGrid.itemRightClicked(index, model, panelPos.x, panelPos.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -701,6 +632,9 @@ DankPopout {
|
||||
property var currentApp: null
|
||||
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) {
|
||||
currentApp = app
|
||||
|
||||
@@ -718,12 +652,8 @@ DankPopout {
|
||||
finalY = y - menuHeight - 8
|
||||
}
|
||||
|
||||
finalX = Math.max(
|
||||
8, Math.min(finalX,
|
||||
appDrawerPopout.popupWidth - menuWidth - 8))
|
||||
finalY = Math.max(8, Math.min(
|
||||
finalY,
|
||||
appDrawerPopout.popupHeight - menuHeight - 8))
|
||||
finalX = Math.max(8, Math.min(finalX, appDrawerPopout.popupWidth - menuWidth - 8))
|
||||
finalY = Math.max(8, Math.min(finalY, appDrawerPopout.popupHeight - menuHeight - 8))
|
||||
|
||||
contextMenu.x = finalX
|
||||
contextMenu.y = finalY
|
||||
@@ -743,9 +673,8 @@ DankPopout {
|
||||
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
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
z: 1000
|
||||
opacity: menuVisible ? 1 : 0
|
||||
scale: menuVisible ? 1 : 0.85
|
||||
@@ -772,11 +701,7 @@ DankPopout {
|
||||
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"
|
||||
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
@@ -785,17 +710,7 @@ DankPopout {
|
||||
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"
|
||||
}
|
||||
name: contextMenu.isPinned ? "keep_off" : "push_pin"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
@@ -803,17 +718,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
text: contextMenu.isPinned ? "Unpin from Dock" : "Pin to Dock"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
@@ -828,17 +733,15 @@ DankPopout {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!contextMenu.currentApp
|
||||
|| !contextMenu.currentApp.desktopEntry)
|
||||
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)
|
||||
if (contextMenu.isPinned) {
|
||||
SessionData.removePinnedApp(contextMenu.appId)
|
||||
} else {
|
||||
SessionData.addPinnedApp(contextMenu.appId)
|
||||
}
|
||||
contextMenu.close()
|
||||
}
|
||||
}
|
||||
@@ -854,8 +757,7 @@ DankPopout {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,11 +765,7 @@ DankPopout {
|
||||
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"
|
||||
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -17,20 +17,13 @@ Item {
|
||||
property bool debounceSearch: true
|
||||
property int debounceInterval: 50
|
||||
property bool keyboardNavigationActive: false
|
||||
property var categories: {
|
||||
var allCategories = AppSearchService.getAllCategories().filter(cat => {
|
||||
return cat !== "Education"
|
||||
&& cat !== "Science"
|
||||
})
|
||||
var result = ["All"]
|
||||
return result.concat(allCategories.filter(cat => {
|
||||
return cat !== "All"
|
||||
}))
|
||||
property bool suppressUpdatesWhileLaunching: false
|
||||
readonly property var categories: {
|
||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||
const result = ["All"]
|
||||
return result.concat(allCategories.filter(cat => cat !== "All"))
|
||||
}
|
||||
property var categoryIcons: categories.map(category => {
|
||||
return AppSearchService.getCategoryIcon(
|
||||
category)
|
||||
})
|
||||
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
|
||||
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
||||
property alias model: filteredModel
|
||||
property var _watchApplications: AppSearchService.applications
|
||||
@@ -40,120 +33,105 @@ Item {
|
||||
signal viewModeSelected(string mode)
|
||||
|
||||
function updateFilteredModel() {
|
||||
if (suppressUpdatesWhileLaunching) {
|
||||
suppressUpdatesWhileLaunching = false
|
||||
return
|
||||
}
|
||||
filteredModel.clear()
|
||||
selectedIndex = 0
|
||||
keyboardNavigationActive = false
|
||||
var apps = []
|
||||
|
||||
let apps = []
|
||||
if (searchQuery.length === 0) {
|
||||
if (selectedCategory === "All") {
|
||||
apps = AppSearchService.getAppsInCategory(
|
||||
"All") // HACK: Use function call instead of property
|
||||
} else {
|
||||
var categoryApps = AppSearchService.getAppsInCategory(
|
||||
selectedCategory)
|
||||
apps = categoryApps.slice(0, maxResults)
|
||||
}
|
||||
apps = selectedCategory === "All" ? AppSearchService.getAppsInCategory("All") : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
||||
} else {
|
||||
if (selectedCategory === "All") {
|
||||
apps = AppSearchService.searchApplications(searchQuery)
|
||||
} else {
|
||||
var categoryApps = AppSearchService.getAppsInCategory(
|
||||
selectedCategory)
|
||||
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
||||
if (categoryApps.length > 0) {
|
||||
var allSearchResults = AppSearchService.searchApplications(
|
||||
searchQuery)
|
||||
var categoryNames = new Set(categoryApps.map(app => {
|
||||
return app.name
|
||||
}))
|
||||
apps = allSearchResults.filter(searchApp => {
|
||||
return categoryNames.has(
|
||||
searchApp.name)
|
||||
}).slice(0, maxResults)
|
||||
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
||||
const categoryNames = new Set(categoryApps.map(app => app.name))
|
||||
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
||||
} else {
|
||||
apps = []
|
||||
}
|
||||
}
|
||||
}
|
||||
if (searchQuery.length === 0)
|
||||
apps = apps.sort(function (a, b) {
|
||||
var aId = a.id || (a.execString || a.exec || "")
|
||||
var bId = b.id || (b.execString || b.exec || "")
|
||||
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
||||
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
||||
if (aUsage !== bUsage)
|
||||
return bUsage - aUsage
|
||||
|
||||
return (a.name || "").localeCompare(b.name || "")
|
||||
})
|
||||
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 => {
|
||||
if (app)
|
||||
filteredModel.append({
|
||||
"name": app.name || "",
|
||||
"exec": app.execString || "",
|
||||
"icon": app.icon
|
||||
|| "application-x-executable",
|
||||
"comment": app.comment || "",
|
||||
"categories": app.categories
|
||||
|| [],
|
||||
"desktopEntry": app
|
||||
})
|
||||
if (app) {
|
||||
filteredModel.append({
|
||||
"name": app.name || "",
|
||||
"exec": app.execString || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
"comment": app.comment || "",
|
||||
"categories": app.categories || [],
|
||||
"desktopEntry": app
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredModel.count > 0) {
|
||||
keyboardNavigationActive = true
|
||||
if (viewMode === "grid") {
|
||||
var newIndex = Math.min(selectedIndex + gridColumns,
|
||||
filteredModel.count - 1)
|
||||
selectedIndex = newIndex
|
||||
} else {
|
||||
selectedIndex = Math.min(selectedIndex + 1,
|
||||
filteredModel.count - 1)
|
||||
}
|
||||
if (filteredModel.count === 0) {
|
||||
return
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredModel.count > 0) {
|
||||
keyboardNavigationActive = true
|
||||
if (viewMode === "grid") {
|
||||
var newIndex = Math.max(selectedIndex - gridColumns, 0)
|
||||
selectedIndex = newIndex
|
||||
} else {
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
if (filteredModel.count === 0) {
|
||||
return
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
|
||||
function selectNextInRow() {
|
||||
if (filteredModel.count > 0 && viewMode === "grid") {
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||
return
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||
}
|
||||
|
||||
function selectPreviousInRow() {
|
||||
if (filteredModel.count > 0 && viewMode === "grid") {
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||
return
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
|
||||
function launchSelected() {
|
||||
if (filteredModel.count > 0 && selectedIndex >= 0
|
||||
&& selectedIndex < filteredModel.count) {
|
||||
var selectedApp = filteredModel.get(selectedIndex)
|
||||
launchApp(selectedApp)
|
||||
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
|
||||
return
|
||||
}
|
||||
const selectedApp = filteredModel.get(selectedIndex)
|
||||
launchApp(selectedApp)
|
||||
}
|
||||
|
||||
function launchApp(appData) {
|
||||
if (!appData)
|
||||
if (!appData) {
|
||||
return
|
||||
|
||||
appData.desktopEntry.execute()
|
||||
}
|
||||
suppressUpdatesWhileLaunching = true
|
||||
SessionService.launchDesktopEntry(appData.desktopEntry)
|
||||
appLaunched(appData)
|
||||
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
||||
}
|
||||
@@ -169,10 +147,11 @@ Item {
|
||||
}
|
||||
|
||||
onSearchQueryChanged: {
|
||||
if (debounceSearch)
|
||||
if (debounceSearch) {
|
||||
searchDebounceTimer.restart()
|
||||
else
|
||||
} else {
|
||||
updateFilteredModel()
|
||||
}
|
||||
}
|
||||
onSelectedCategoryChanged: updateFilteredModel()
|
||||
onAppUsageRankingChanged: updateFilteredModel()
|
||||
|
||||
@@ -8,11 +8,25 @@ Item {
|
||||
|
||||
property var categories: []
|
||||
property string selectedCategory: "All"
|
||||
property bool compact: false // For different layout styles
|
||||
property bool compact: false
|
||||
|
||||
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: "transparent"
|
||||
|
||||
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 {
|
||||
visible: compact
|
||||
@@ -20,22 +34,15 @@ Item {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: categories.slice(0, Math.min(categories.length,
|
||||
8)) // Limit for space
|
||||
model: categories ? categories.slice(0, Math.min(categories.length || 0, maxCompactItems)) : []
|
||||
|
||||
Rectangle {
|
||||
height: 36
|
||||
width: (parent.width - (Math.min(
|
||||
categories.length,
|
||||
8) - 1) * Theme.spacingS) / Math.min(
|
||||
categories.length, 8)
|
||||
property int itemCount: Math.min(categories ? categories.length || 0 : 0, maxCompactItems)
|
||||
|
||||
height: root.itemHeight
|
||||
width: root.getButtonWidth(itemCount, parent.width)
|
||||
radius: Theme.cornerRadius
|
||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(
|
||||
Theme.outline.r,
|
||||
Theme.outline.g,
|
||||
Theme.outline.b,
|
||||
0.3)
|
||||
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
@@ -50,10 +57,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedCategory = modelData
|
||||
categorySelected(modelData)
|
||||
}
|
||||
onClicked: root.handleCategoryClick(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,27 +69,20 @@ Item {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
property var firstRowCategories: categories.slice(
|
||||
0, Math.min(4,
|
||||
categories.length))
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: parent.firstRowCategories
|
||||
model: categories ? categories.slice(0, Math.min(4, categories.length || 0)) : []
|
||||
|
||||
Rectangle {
|
||||
height: 36
|
||||
width: (parent.width - (parent.firstRowCategories.length - 1)
|
||||
* Theme.spacingS) / parent.firstRowCategories.length
|
||||
property int itemCount: Math.min(4, categories ? categories.length || 0 : 0)
|
||||
|
||||
height: root.itemHeight
|
||||
width: root.getButtonWidth(itemCount, parent.width)
|
||||
radius: Theme.cornerRadius
|
||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||
border.color: selectedCategory
|
||||
=== modelData ? "transparent" : Qt.rgba(
|
||||
Theme.outline.r,
|
||||
Theme.outline.g,
|
||||
Theme.outline.b, 0.3)
|
||||
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
|
||||
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
@@ -100,37 +97,28 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedCategory = modelData
|
||||
categorySelected(modelData)
|
||||
}
|
||||
onClicked: root.handleCategoryClick(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
property var secondRowCategories: categories.slice(
|
||||
4, categories.length)
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: secondRowCategories.length > 0
|
||||
visible: categories && categories.length > 4
|
||||
|
||||
Repeater {
|
||||
model: parent.secondRowCategories
|
||||
model: categories && categories.length > 4 ? categories.slice(4) : []
|
||||
|
||||
Rectangle {
|
||||
height: 36
|
||||
width: (parent.width - (parent.secondRowCategories.length - 1)
|
||||
* Theme.spacingS) / parent.secondRowCategories.length
|
||||
property int itemCount: categories && categories.length > 4 ? categories.length - 4 : 0
|
||||
|
||||
height: root.itemHeight
|
||||
width: root.getButtonWidth(itemCount, parent.width)
|
||||
radius: Theme.cornerRadius
|
||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||
border.color: selectedCategory
|
||||
=== modelData ? "transparent" : Qt.rgba(
|
||||
Theme.outline.r,
|
||||
Theme.outline.g,
|
||||
Theme.outline.b, 0.3)
|
||||
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
|
||||
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
@@ -145,10 +133,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedCategory = modelData
|
||||
categorySelected(modelData)
|
||||
}
|
||||
onClicked: root.handleCategoryClick(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: calendarGrid
|
||||
|
||||
property date displayDate: new Date()
|
||||
property date selectedDate: new Date()
|
||||
|
||||
function loadEventsForMonth() {
|
||||
if (!CalendarService || !CalendarService.khalAvailable)
|
||||
return
|
||||
|
||||
let firstDay = new Date(displayDate.getFullYear(),
|
||||
displayDate.getMonth(), 1)
|
||||
let dayOfWeek = firstDay.getDay()
|
||||
let startDate = new Date(firstDay)
|
||||
startDate.setDate(startDate.getDate(
|
||||
) - dayOfWeek - 7) // Extra week padding
|
||||
let lastDay = new Date(displayDate.getFullYear(),
|
||||
displayDate.getMonth() + 1, 0)
|
||||
let endDate = new Date(lastDay)
|
||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay(
|
||||
)) + 7) // Extra week padding
|
||||
CalendarService.loadEvents(startDate, endDate)
|
||||
}
|
||||
|
||||
spacing: Theme.spacingM
|
||||
onDisplayDateChanged: {
|
||||
loadEventsForMonth()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
loadEventsForMonth()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKhalAvailableChanged() {
|
||||
if (CalendarService && CalendarService.khalAvailable)
|
||||
loadEventsForMonth()
|
||||
}
|
||||
|
||||
target: CalendarService
|
||||
enabled: CalendarService !== null
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "chevron_left"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevMonthArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let newDate = new Date(displayDate)
|
||||
newDate.setMonth(newDate.getMonth() - 1)
|
||||
displayDate = newDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 80
|
||||
height: 40
|
||||
text: displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "chevron_right"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextMonthArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let newDate = new Date(displayDate)
|
||||
newDate.setMonth(newDate.getMonth() + 1)
|
||||
displayDate = newDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
var days = []
|
||||
var locale = Qt.locale()
|
||||
for (var i = 0; i < 7; i++) {
|
||||
var date = new Date(2024, 0, 7 + i)
|
||||
days.push(locale.dayName(i, Locale.ShortFormat))
|
||||
}
|
||||
return days
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 7
|
||||
height: 32
|
||||
color: "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.6)
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
property date firstDay: {
|
||||
let date = new Date(displayDate.getFullYear(),
|
||||
displayDate.getMonth(), 1)
|
||||
let dayOfWeek = date.getDay()
|
||||
date.setDate(date.getDate() - dayOfWeek)
|
||||
return date
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: 200 // Fixed height for calendar
|
||||
columns: 7
|
||||
rows: 6
|
||||
|
||||
Repeater {
|
||||
model: 42
|
||||
|
||||
Rectangle {
|
||||
property date dayDate: {
|
||||
let date = new Date(parent.firstDay)
|
||||
date.setDate(date.getDate() + index)
|
||||
return date
|
||||
}
|
||||
property bool isCurrentMonth: dayDate.getMonth(
|
||||
) === displayDate.getMonth()
|
||||
property bool isToday: dayDate.toDateString(
|
||||
) === new Date().toDateString()
|
||||
property bool isSelected: dayDate.toDateString(
|
||||
) === selectedDate.toDateString()
|
||||
|
||||
width: parent.width / 7
|
||||
height: parent.height / 6
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
color: isSelected ? Theme.primary : isToday ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
radius: Theme.cornerRadius
|
||||
clip: true
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: dayDate.getDate()
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
font.weight: isToday
|
||||
|| isSelected ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: eventIndicator
|
||||
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
visible: CalendarService
|
||||
&& CalendarService.khalAvailable
|
||||
&& CalendarService.hasEventsForDate(dayDate)
|
||||
opacity: {
|
||||
if (isSelected)
|
||||
return 0.9
|
||||
else if (isToday)
|
||||
return 0.8
|
||||
else
|
||||
return 0.6
|
||||
}
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.89
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 0.9
|
||||
color: {
|
||||
if (isSelected)
|
||||
return Qt.lighter(Theme.primary, 1.3)
|
||||
else if (isToday)
|
||||
return Theme.primary
|
||||
else
|
||||
return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: {
|
||||
if (isSelected)
|
||||
return Qt.lighter(Theme.primary, 1.3)
|
||||
else if (isToday)
|
||||
return Theme.primary
|
||||
else
|
||||
return Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dayArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedDate = dayDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modules.CentcomCenter
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
readonly property bool hasActiveMedia: MprisController.activePlayer !== null
|
||||
property bool calendarVisible: false
|
||||
property bool shouldBeVisible: false
|
||||
property real triggerX: (Screen.width - 480) / 2
|
||||
property real triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4
|
||||
property real triggerWidth: 80
|
||||
property string triggerSection: "center"
|
||||
property var triggerScreen: null
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerX = x
|
||||
triggerY = y
|
||||
triggerWidth = width
|
||||
triggerSection = section
|
||||
triggerScreen = screen
|
||||
}
|
||||
|
||||
visible: calendarVisible || closeTimer.running
|
||||
screen: triggerScreen
|
||||
onCalendarVisibleChanged: {
|
||||
if (calendarVisible) {
|
||||
closeTimer.stop()
|
||||
shouldBeVisible = true
|
||||
visible = true
|
||||
Qt.callLater(() => {
|
||||
calendarGrid.loadEventsForMonth()
|
||||
})
|
||||
} else {
|
||||
shouldBeVisible = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: Theme.mediumDuration + 50
|
||||
onTriggered: {
|
||||
if (!shouldBeVisible) {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (visible && calendarGrid)
|
||||
calendarGrid.loadEventsForMonth()
|
||||
}
|
||||
implicitWidth: 480
|
||||
implicitHeight: 600
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mainContainer
|
||||
|
||||
readonly property real targetWidth: Math.min(
|
||||
(root.screen ? root.screen.width : Screen.width)
|
||||
* 0.9, 600)
|
||||
|
||||
function calculateWidth() {
|
||||
let baseWidth = 320
|
||||
if (leftWidgets.hasAnyWidgets)
|
||||
return Math.min(parent.width * 0.9, 600)
|
||||
|
||||
return Math.min(parent.width * 0.7, 400)
|
||||
}
|
||||
|
||||
function calculateHeight() {
|
||||
let contentHeight = Theme.spacingM * 2
|
||||
// margins
|
||||
let widgetHeight = 160
|
||||
widgetHeight += 140 + Theme.spacingM
|
||||
let calendarHeight = 300
|
||||
let mainRowHeight = Math.max(widgetHeight, calendarHeight)
|
||||
contentHeight += mainRowHeight + Theme.spacingM
|
||||
if (CalendarService && CalendarService.khalAvailable) {
|
||||
let hasEvents = events.selectedDateEvents
|
||||
&& events.selectedDateEvents.length > 0
|
||||
let eventsHeight = hasEvents ? Math.min(
|
||||
300,
|
||||
80 + events.selectedDateEvents.length * 60) : 120
|
||||
contentHeight += eventsHeight
|
||||
} else {
|
||||
contentHeight -= Theme.spacingM
|
||||
}
|
||||
return Math.min(contentHeight, parent.height * 0.9)
|
||||
}
|
||||
|
||||
readonly property real calculatedX: {
|
||||
var screenWidth = root.screen ? root.screen.width : Screen.width
|
||||
if (root.triggerSection === "center") {
|
||||
return (screenWidth - targetWidth) / 2
|
||||
}
|
||||
|
||||
var centerX = root.triggerX + (root.triggerWidth / 2) - (targetWidth / 2)
|
||||
|
||||
if (centerX >= Theme.spacingM
|
||||
&& centerX + targetWidth <= screenWidth - Theme.spacingM) {
|
||||
return centerX
|
||||
}
|
||||
|
||||
if (centerX < Theme.spacingM) {
|
||||
return Theme.spacingM
|
||||
}
|
||||
|
||||
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
|
||||
return screenWidth - targetWidth - Theme.spacingM
|
||||
}
|
||||
|
||||
return centerX
|
||||
}
|
||||
|
||||
width: targetWidth
|
||||
height: calculateHeight()
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
scale: shouldBeVisible ? 1 : 0.9
|
||||
x: calculatedX
|
||||
y: root.triggerY
|
||||
onOpacityChanged: {
|
||||
if (opacity === 1)
|
||||
Qt.callLater(() => {
|
||||
height = calculateHeight()
|
||||
})
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onEventsByDateChanged() {
|
||||
if (mainContainer.opacity === 1)
|
||||
mainContainer.height = mainContainer.calculateHeight()
|
||||
}
|
||||
|
||||
function onKhalAvailableChanged() {
|
||||
if (mainContainer.opacity === 1)
|
||||
mainContainer.height = mainContainer.calculateHeight()
|
||||
}
|
||||
|
||||
target: CalendarService
|
||||
enabled: CalendarService !== null
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onSelectedDateEventsChanged() {
|
||||
if (mainContainer.opacity === 1)
|
||||
mainContainer.height = mainContainer.calculateHeight()
|
||||
}
|
||||
|
||||
target: events
|
||||
enabled: events !== null
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
|
||||
Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: shouldBeVisible
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.08
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.02
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
focus: true
|
||||
Keys.onPressed: function (event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
calendarVisible = false
|
||||
event.accepted = true
|
||||
} else {
|
||||
// Don't handle other keys - let them bubble up to modals
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: {
|
||||
let widgetHeight = 160
|
||||
// Media widget
|
||||
widgetHeight += 140 + Theme.spacingM // Weather/SystemInfo widget with spacing
|
||||
let calendarHeight = 300
|
||||
// Calendar
|
||||
return Math.max(widgetHeight, calendarHeight)
|
||||
}
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
id: leftWidgets
|
||||
|
||||
property bool hasAnyWidgets: true
|
||||
|
||||
width: hasAnyWidgets ? parent.width
|
||||
* 0.42 : 0 // Slightly narrower for better proportions
|
||||
height: childrenRect.height
|
||||
spacing: Theme.spacingM
|
||||
visible: hasAnyWidgets
|
||||
anchors.top: parent.top
|
||||
|
||||
MediaPlayer {
|
||||
width: parent.width
|
||||
height: 160
|
||||
}
|
||||
|
||||
Weather {
|
||||
width: parent.width
|
||||
height: 140
|
||||
visible: SettingsData.weatherEnabled
|
||||
}
|
||||
|
||||
SystemInfo {
|
||||
width: parent.width
|
||||
height: 140
|
||||
visible: !SettingsData.weatherEnabled
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width
|
||||
- Theme.spacingM : parent.width
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b, 0.2)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
CalendarGrid {
|
||||
id: calendarGrid
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Events {
|
||||
id: events
|
||||
|
||||
width: parent.width
|
||||
selectedDate: calendarGrid.selectedDate
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Anims.durMed
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Anims.durMed
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.5
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.15)
|
||||
shadowOpacity: 0.15
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
enabled: shouldBeVisible
|
||||
onClicked: function (mouse) {
|
||||
var localPos = mapToItem(mainContainer, mouse.x, mouse.y)
|
||||
if (localPos.x < 0 || localPos.x > mainContainer.width
|
||||
|| localPos.y < 0 || localPos.y > mainContainer.height)
|
||||
calendarVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: events
|
||||
|
||||
property date selectedDate: new Date()
|
||||
property var selectedDateEvents: []
|
||||
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
|
||||
property bool shouldShow: CalendarService && CalendarService.khalAvailable
|
||||
|
||||
function updateSelectedDateEvents() {
|
||||
if (CalendarService && CalendarService.khalAvailable) {
|
||||
let events = CalendarService.getEventsForDate(selectedDate)
|
||||
selectedDateEvents = events
|
||||
} else {
|
||||
selectedDateEvents = []
|
||||
}
|
||||
}
|
||||
|
||||
onSelectedDateEventsChanged: {
|
||||
eventsList.model = selectedDateEvents
|
||||
}
|
||||
width: parent.width
|
||||
height: shouldShow ? (hasEvents ? Math.min(
|
||||
300,
|
||||
80 + selectedDateEvents.length * 60) : 120) : 0
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
visible: shouldShow
|
||||
layer.enabled: true
|
||||
Component.onCompleted: {
|
||||
updateSelectedDateEvents()
|
||||
}
|
||||
onSelectedDateChanged: {
|
||||
updateSelectedDateEvents()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onEventsByDateChanged() {
|
||||
updateSelectedDateEvents()
|
||||
}
|
||||
|
||||
function onKhalAvailableChanged() {
|
||||
updateSelectedDateEvents()
|
||||
}
|
||||
|
||||
target: CalendarService
|
||||
enabled: CalendarService !== null
|
||||
}
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "event"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • "
|
||||
+ (selectedDateEvents.length
|
||||
=== 1 ? "1 event" : selectedDateEvents.length
|
||||
+ " events")) : Qt.formatDate(
|
||||
selectedDate, "MMM d")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
visible: !hasEvents
|
||||
|
||||
DankIcon {
|
||||
name: "event_busy"
|
||||
size: Theme.iconSize + 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.3)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No events"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.5)
|
||||
font.weight: Font.Normal
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: eventsList
|
||||
|
||||
anchors.top: headerRow.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingM
|
||||
visible: opacity > 0
|
||||
opacity: hasEvents ? 1 : 0
|
||||
clip: true
|
||||
spacing: Theme.spacingS
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
||||
WheelHandler {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
property real momentum: 0
|
||||
onWheel: event => {
|
||||
if (event.pixelDelta.y !== 0) {
|
||||
// Touchpad with pixel delta
|
||||
momentum = event.pixelDelta.y * 1.8
|
||||
} else {
|
||||
// Mouse wheel with angle delta
|
||||
momentum = (event.angleDelta.y / 120)
|
||||
* (60 * 2.5) // ~2.5 items per wheel step
|
||||
}
|
||||
|
||||
let newY = parent.contentY - momentum
|
||||
newY = Math.max(
|
||||
0, Math.min(parent.contentHeight - parent.height,
|
||||
newY))
|
||||
parent.contentY = newY
|
||||
momentum *= 0.92 // Decay for smooth momentum
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: eventsList.contentHeight
|
||||
> eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: eventsList.width
|
||||
height: eventContent.implicitHeight + Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (modelData.url && eventMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
||||
Theme.primary.b, 0.12)
|
||||
else if (eventMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
||||
Theme.primary.b, 0.06)
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b, 0.06)
|
||||
}
|
||||
border.color: {
|
||||
if (modelData.url && eventMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
||||
Theme.primary.b, 0.3)
|
||||
else if (eventMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
||||
Theme.primary.b, 0.15)
|
||||
return "transparent"
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: parent.height - 8
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 2
|
||||
color: Theme.primary
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
Column {
|
||||
id: eventContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingL + 4
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: 6
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData.title
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(timeRow.height, locationRow.height)
|
||||
|
||||
Row {
|
||||
id: timeRow
|
||||
|
||||
spacing: 4
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "schedule"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r,
|
||||
Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.7)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (modelData.allDay) {
|
||||
return "All day"
|
||||
} else {
|
||||
let timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
||||
let startTime = Qt.formatTime(
|
||||
modelData.start, timeFormat)
|
||||
if (modelData.start.toDateString(
|
||||
) !== modelData.end.toDateString(
|
||||
) || modelData.start.getTime(
|
||||
) !== modelData.end.getTime())
|
||||
return startTime + " – " + Qt.formatTime(
|
||||
modelData.end, timeFormat)
|
||||
|
||||
return startTime
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r,
|
||||
Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.7)
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: locationRow
|
||||
|
||||
spacing: 4
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: modelData.location !== ""
|
||||
|
||||
DankIcon {
|
||||
name: "location_on"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r,
|
||||
Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.7)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.location
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r,
|
||||
Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.7)
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
maximumLineCount: 1
|
||||
width: Math.min(implicitWidth, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: eventMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: modelData.url !== ""
|
||||
onClicked: {
|
||||
if (modelData.url && modelData.url !== "") {
|
||||
if (Qt.openUrlExternally(modelData.url) === false)
|
||||
console.warn("Failed to open URL: " + modelData.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 2
|
||||
shadowBlur: 0.25
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
||||
shadowOpacity: 0.1
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: mediaPlayer
|
||||
|
||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
property string lastValidTitle: ""
|
||||
property string lastValidArtist: ""
|
||||
property string lastValidAlbum: ""
|
||||
property string lastValidArtUrl: ""
|
||||
property real currentPosition: activePlayer
|
||||
&& activePlayer.positionSupported ? activePlayer.position : 0
|
||||
property real displayPosition: currentPosition
|
||||
|
||||
function ratio() {
|
||||
if (!activePlayer || activePlayer.length <= 0) {
|
||||
return 0
|
||||
}
|
||||
let calculatedRatio = displayPosition / activePlayer.length
|
||||
return Math.max(0, Math.min(1, calculatedRatio))
|
||||
}
|
||||
|
||||
onActivePlayerChanged: {
|
||||
if (activePlayer && activePlayer.positionSupported) {
|
||||
currentPosition = Qt.binding(() => activePlayer.position)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: positionTimer
|
||||
interval: 300
|
||||
running: activePlayer
|
||||
&& activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
&& !progressMouseArea.isSeeking
|
||||
repeat: true
|
||||
onTriggered: activePlayer && activePlayer.positionSupported && activePlayer.positionChanged()
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
||||
Theme.surfaceContainer.b, 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
|
||||
|
||||
Timer {
|
||||
id: cleanupTimer
|
||||
|
||||
interval: 2000
|
||||
running: !activePlayer
|
||||
onTriggered: {
|
||||
lastValidTitle = ""
|
||||
lastValidArtist = ""
|
||||
lastValidAlbum = ""
|
||||
lastValidArtUrl = ""
|
||||
currentPosition = 0
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: (!activePlayer && !lastValidTitle)
|
||||
|| (activePlayer && activePlayer.trackTitle === ""
|
||||
&& lastValidTitle === "")
|
||||
|
||||
DankIcon {
|
||||
name: "music_note"
|
||||
size: Theme.iconSize + 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No Media Playing"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: activePlayer && activePlayer.trackTitle !== ""
|
||||
|| lastValidTitle !== ""
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 60
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 60
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
id: albumArt
|
||||
|
||||
anchors.fill: parent
|
||||
source: activePlayer && activePlayer.trackArtUrl
|
||||
|| lastValidArtUrl || ""
|
||||
onSourceChanged: {
|
||||
if (activePlayer && activePlayer.trackArtUrl
|
||||
&& albumArt.status !== Image.Error)
|
||||
lastValidArtUrl = activePlayer.trackArtUrl
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
cache: true
|
||||
asynchronous: true
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
console.warn("Failed to load album art:", source)
|
||||
source = ""
|
||||
if (activePlayer && activePlayer.trackArtUrl === source) {
|
||||
lastValidArtUrl = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: albumArt.status !== Image.Ready || !albumArt.source
|
||||
color: "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "album"
|
||||
size: 28
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 60 - Theme.spacingM
|
||||
height: parent.height
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: activePlayer && activePlayer.trackTitle
|
||||
|| lastValidTitle || "Unknown Track"
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackTitle)
|
||||
lastValidTitle = activePlayer.trackTitle
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: activePlayer && activePlayer.trackArtist
|
||||
|| lastValidArtist || "Unknown Artist"
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackArtist)
|
||||
lastValidArtist = activePlayer.trackArtist
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r,
|
||||
Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: activePlayer && activePlayer.trackAlbum
|
||||
|| lastValidAlbum || ""
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackAlbum)
|
||||
lastValidAlbum = activePlayer.trackAlbum
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r,
|
||||
Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.6)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: progressBarContainer
|
||||
|
||||
width: parent.width
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
id: progressBarBackground
|
||||
|
||||
width: parent.width
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b, 0.3)
|
||||
visible: activePlayer !== null
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: progressFill
|
||||
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
width: Math.max(0, Math.min(parent.width,
|
||||
parent.width * ratio()))
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: progressHandle
|
||||
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 1
|
||||
x: Math.max(0, Math.min(parent.width - width,
|
||||
progressFill.width - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: activePlayer && activePlayer.length > 0
|
||||
scale: progressMouseArea.containsMouse
|
||||
|| progressMouseArea.pressed ? 1.2 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: progressMouseArea
|
||||
|
||||
property bool isSeeking: false
|
||||
property real pendingSeekPosition: -1
|
||||
|
||||
Timer {
|
||||
id: seekDebounceTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
let clampedPosition = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clampedPosition
|
||||
progressMouseArea.pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.length > 0
|
||||
&& activePlayer.canSeek
|
||||
preventStealing: true
|
||||
onPressed: function (mouse) {
|
||||
isSeeking = true
|
||||
if (activePlayer && activePlayer.length > 0
|
||||
&& activePlayer.canSeek) {
|
||||
let ratio = Math.max(
|
||||
0, Math.min(
|
||||
1,
|
||||
mouse.x / progressBarBackground.width))
|
||||
pendingSeekPosition = ratio * activePlayer.length
|
||||
displayPosition = pendingSeekPosition
|
||||
seekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
isSeeking = false
|
||||
seekDebounceTimer.stop()
|
||||
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
let clampedPosition = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clampedPosition
|
||||
pendingSeekPosition = -1
|
||||
}
|
||||
displayPosition = Qt.binding(() => currentPosition)
|
||||
}
|
||||
onPositionChanged: function (mouse) {
|
||||
if (pressed && isSeeking && activePlayer
|
||||
&& activePlayer.length > 0
|
||||
&& activePlayer.canSeek) {
|
||||
let ratio = Math.max(
|
||||
0, Math.min(
|
||||
1,
|
||||
mouse.x / progressBarBackground.width))
|
||||
pendingSeekPosition = ratio * activePlayer.length
|
||||
displayPosition = pendingSeekPosition
|
||||
seekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (activePlayer && activePlayer.length > 0
|
||||
&& activePlayer.canSeek) {
|
||||
let ratio = Math.max(
|
||||
0, Math.min(
|
||||
1,
|
||||
mouse.x / progressBarBackground.width))
|
||||
activePlayer.position = ratio * activePlayer.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: progressGlobalMouseArea
|
||||
|
||||
x: 0
|
||||
y: 0
|
||||
width: mediaPlayer.width
|
||||
height: mediaPlayer.height
|
||||
enabled: progressMouseArea.isSeeking
|
||||
visible: false
|
||||
preventStealing: true
|
||||
onPositionChanged: function (mouse) {
|
||||
if (progressMouseArea.isSeeking && activePlayer
|
||||
&& activePlayer.length > 0
|
||||
&& activePlayer.canSeek) {
|
||||
let globalPos = mapToItem(progressBarBackground,
|
||||
mouse.x, mouse.y)
|
||||
let ratio = Math.max(
|
||||
0, Math.min(
|
||||
1,
|
||||
globalPos.x / progressBarBackground.width))
|
||||
progressMouseArea.pendingSeekPosition = ratio * activePlayer.length
|
||||
displayPosition = progressMouseArea.pendingSeekPosition
|
||||
seekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
progressMouseArea.isSeeking = false
|
||||
seekDebounceTimer.stop()
|
||||
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
let clampedPosition = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clampedPosition
|
||||
progressMouseArea.pendingSeekPosition = -1
|
||||
}
|
||||
displayPosition = Qt.binding(() => currentPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 32
|
||||
visible: activePlayer !== null
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
height: parent.height
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: prevBtnArea.containsMouse ? Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_previous"
|
||||
size: 16
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevBtnArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!activePlayer)
|
||||
return
|
||||
|
||||
if (activePlayer.position > 8
|
||||
&& activePlayer.canSeek) {
|
||||
activePlayer.position = 0
|
||||
} else {
|
||||
activePlayer.previous()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Theme.primary
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer && activePlayer.playbackState
|
||||
=== MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
size: 20
|
||||
color: Theme.background
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: activePlayer
|
||||
&& activePlayer.togglePlaying()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: nextBtnArea.containsMouse ? Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_next"
|
||||
size: 16
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextBtnArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: activePlayer && activePlayer.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 2
|
||||
shadowBlur: 0.5
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
||||
shadowOpacity: 0.1
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
||||
Theme.surfaceContainer.b, 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Ref {
|
||||
service: DgopService
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
SystemLogo {
|
||||
width: 48
|
||||
height: 48
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 48 - Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: DgopService.hostname
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.distribution + " • " + DgopService.architecture
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "Uptime " + formatUptime(UserInfoService.uptime)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Load: " + DgopService.loadAverage
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.processCount + " proc, " + DgopService.threadCount + " threads"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatUptime(uptime) {
|
||||
if (!uptime)
|
||||
return "0m"
|
||||
|
||||
// Parse the uptime string - handle formats like "1 week, 4 days, 3:45" or "4 days, 3:45" or "3:45"
|
||||
var uptimeStr = uptime.toString().trim()
|
||||
|
||||
// Check for weeks and days - need to add them together
|
||||
var weekMatch = uptimeStr.match(/(\d+)\s+weeks?/)
|
||||
var dayMatch = uptimeStr.match(/(\d+)\s+days?/)
|
||||
|
||||
if (weekMatch) {
|
||||
var weeks = parseInt(weekMatch[1])
|
||||
var totalDays = weeks * 7
|
||||
if (dayMatch) {
|
||||
var days = parseInt(dayMatch[1])
|
||||
totalDays += days
|
||||
}
|
||||
return totalDays + "d"
|
||||
} else if (dayMatch) {
|
||||
var days = parseInt(dayMatch[1])
|
||||
return days + "d"
|
||||
}
|
||||
|
||||
// If it's just hours:minutes, show the largest unit
|
||||
var timeMatch = uptimeStr.match(/(\d+):(\d+)/)
|
||||
if (timeMatch) {
|
||||
var hours = parseInt(timeMatch[1])
|
||||
var minutes = parseInt(timeMatch[2])
|
||||
if (hours > 0) {
|
||||
return hours + "h"
|
||||
} else {
|
||||
return minutes + "m"
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback - return as is but truncated
|
||||
return uptimeStr.length > 8 ? uptimeStr.substring(0,
|
||||
8) + "…" : uptimeStr
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: weather
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
||||
Theme.surfaceContainer.b, 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
|
||||
Ref {
|
||||
service: WeatherService
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: !WeatherService.weather.available
|
||||
|| WeatherService.weather.temp === 0
|
||||
|
||||
DankIcon {
|
||||
name: "cloud_off"
|
||||
size: Theme.iconSize + 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No Weather Data"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
visible: WeatherService.weather.available
|
||||
&& WeatherService.weather.temp !== 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 60
|
||||
|
||||
DankIcon {
|
||||
id: refreshButton
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 6
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.3)
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: -Theme.spacingS
|
||||
anchors.topMargin: -Theme.spacingS
|
||||
|
||||
property bool isRefreshing: false
|
||||
enabled: !isRefreshing
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
||||
onClicked: {
|
||||
refreshButton.isRefreshing = true
|
||||
WeatherService.forceRefresh()
|
||||
refreshTimer.restart()
|
||||
}
|
||||
enabled: parent.enabled
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 2000
|
||||
onTriggered: refreshButton.isRefreshing = false
|
||||
}
|
||||
|
||||
NumberAnimation on rotation {
|
||||
running: refreshButton.isRefreshing
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
|
||||
DankIcon {
|
||||
name: WeatherService.getWeatherIcon(
|
||||
WeatherService.weather.wCode)
|
||||
size: Theme.iconSize + 8
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp)
|
||||
+ "°" + (SettingsData.useFahrenheit ? "F" : "C")
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Light
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (WeatherService.weather.available)
|
||||
SettingsData.setTemperatureUnit(
|
||||
!SettingsData.useFahrenheit)
|
||||
}
|
||||
enabled: WeatherService.weather.available
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.city || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r,
|
||||
Theme.surfaceText.g,
|
||||
Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
columns: 2
|
||||
spacing: Theme.spacingM
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "humidity_low"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.humidity ? WeatherService.weather.humidity
|
||||
+ "%" : "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "air"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.wind || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "wb_twilight"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.sunrise || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.sunset || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 2
|
||||
shadowBlur: 0.5
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
||||
shadowOpacity: 0.1
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
Modules/ControlCenter/Components/DragDropDetailHost.qml
Normal file
91
Modules/ControlCenter/Components/DragDropDetailHost.qml
Normal file
@@ -0,0 +1,91 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter.Details
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string expandedSection: ""
|
||||
property var expandedWidgetData: null
|
||||
|
||||
height: active ? 250 : 0
|
||||
visible: active
|
||||
|
||||
readonly property bool active: expandedSection !== ""
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingS
|
||||
sourceComponent: {
|
||||
if (!root.active) return null
|
||||
|
||||
if (expandedSection.startsWith("diskUsage_")) {
|
||||
return diskUsageDetailComponent
|
||||
}
|
||||
|
||||
switch (expandedSection) {
|
||||
case "wifi": return networkDetailComponent
|
||||
case "bluetooth": return bluetoothDetailComponent
|
||||
case "audioOutput": return audioOutputDetailComponent
|
||||
case "audioInput": return audioInputDetailComponent
|
||||
case "battery": return batteryDetailComponent
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
672
Modules/ControlCenter/Components/DragDropGrid.qml
Normal file
672
Modules/ControlCenter/Components/DragDropGrid.qml
Normal file
@@ -0,0 +1,672 @@
|
||||
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
|
||||
}
|
||||
|
||||
function moveToTop(item) {
|
||||
const children = root.children
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
if (children[i] === item)
|
||||
continue
|
||||
if (children[i].z)
|
||||
children[i].z = Math.min(children[i].z, 999)
|
||||
}
|
||||
item.z = 1000
|
||||
}
|
||||
|
||||
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 : -6) : 0
|
||||
bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: rowWidgets || []
|
||||
|
||||
DragDropWidgetWrapper {
|
||||
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: isSliderOnlyRow ? 48 : 60
|
||||
|
||||
editMode: root.editMode
|
||||
widgetIndex: globalWidgetIndex
|
||||
gridCellWidth: width
|
||||
gridCellHeight: height
|
||||
gridColumns: 4
|
||||
gridLayout: root
|
||||
isSlider: {
|
||||
const id = modelData.id || ""
|
||||
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"
|
||||
}
|
||||
|
||||
widgetComponent: {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||
onRemoveWidget: index => root.removeWidget(index)
|
||||
onToggleWidgetSize: index => root.toggleWidgetSize(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (root.editMode) return
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: audioSliderComponent
|
||||
Item {
|
||||
property var widgetData: parent.widgetData || {}
|
||||
property int widgetIndex: parent.widgetIndex || 0
|
||||
width: parent.width
|
||||
height: 16
|
||||
|
||||
AudioSliderRow {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 14
|
||||
property color sliderTrackColor: Theme.surfaceContainerHigh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: toggleButtonComponent
|
||||
ToggleButton {
|
||||
property var widgetData: parent.widgetData || {}
|
||||
property int widgetIndex: parent.widgetIndex || 0
|
||||
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 "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 "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
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: !root.editMode
|
||||
|
||||
onClicked: {
|
||||
if (root.editMode)
|
||||
return
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: smallToggleComponent
|
||||
SmallToggleButton {
|
||||
property var widgetData: parent.widgetData || {}
|
||||
property int widgetIndex: parent.widgetIndex || 0
|
||||
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 "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: !root.editMode
|
||||
|
||||
onClicked: {
|
||||
if (root.editMode)
|
||||
return
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
289
Modules/ControlCenter/Components/DragDropWidgetWrapper.qml
Normal file
289
Modules/ControlCenter/Components/DragDropWidgetWrapper.qml
Normal file
@@ -0,0 +1,289 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool editMode: false
|
||||
property var widgetData: null
|
||||
property int widgetIndex: -1
|
||||
property bool isSlider: false
|
||||
property Component widgetComponent: null
|
||||
property real gridCellWidth: 100
|
||||
property real gridCellHeight: 60
|
||||
property int gridColumns: 4
|
||||
property var gridLayout: null
|
||||
|
||||
z: dragArea.drag.active ? 10000 : 1
|
||||
|
||||
signal widgetMoved(int fromIndex, int toIndex)
|
||||
signal removeWidget(int index)
|
||||
signal toggleWidgetSize(int index)
|
||||
|
||||
width: {
|
||||
const widgetWidth = widgetData?.width || 50
|
||||
if (widgetWidth <= 25) return gridCellWidth
|
||||
else if (widgetWidth <= 50) return gridCellWidth * 2
|
||||
else if (widgetWidth <= 75) return gridCellWidth * 3
|
||||
else return gridCellWidth * 4
|
||||
}
|
||||
height: isSlider ? 16 : gridCellHeight
|
||||
|
||||
Rectangle {
|
||||
id: dragIndicator
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: dragArea.drag.active ? 2 : 0
|
||||
radius: Theme.cornerRadius
|
||||
opacity: dragArea.drag.active ? 0.8 : 1.0
|
||||
z: dragArea.drag.active ? 10000 : 1
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: widgetLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: widgetComponent
|
||||
property var widgetData: root.widgetData
|
||||
property int widgetIndex: root.widgetIndex
|
||||
property int globalWidgetIndex: root.widgetIndex
|
||||
property int widgetWidth: root.widgetData?.width || 50
|
||||
|
||||
|
||||
MouseArea {
|
||||
id: editModeBlocker
|
||||
anchors.fill: parent
|
||||
enabled: root.editMode
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: function(mouse) { mouse.accepted = true }
|
||||
onWheel: function(wheel) { wheel.accepted = true }
|
||||
z: 100
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
anchors.fill: parent
|
||||
enabled: editMode
|
||||
cursorShape: editMode ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||
drag.target: editMode ? root : null
|
||||
drag.axis: Drag.XAndYAxis
|
||||
drag.smoothed: true
|
||||
|
||||
onPressed: function(mouse) {
|
||||
if (editMode) {
|
||||
cursorShape = Qt.ClosedHandCursor
|
||||
if (root.gridLayout && root.gridLayout.moveToTop) {
|
||||
root.gridLayout.moveToTop(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: function(mouse) {
|
||||
if (editMode) {
|
||||
cursorShape = Qt.OpenHandCursor
|
||||
root.snapToGrid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Drag.active: dragArea.drag.active
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
|
||||
function swapIndices(i, j) {
|
||||
if (i === j) return;
|
||||
const arr = SettingsData.controlCenterWidgets;
|
||||
if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) return;
|
||||
|
||||
const copy = arr.slice();
|
||||
const tmp = copy[i];
|
||||
copy[i] = copy[j];
|
||||
copy[j] = tmp;
|
||||
|
||||
SettingsData.setControlCenterWidgets(copy);
|
||||
}
|
||||
|
||||
function snapToGrid() {
|
||||
if (!editMode || !gridLayout) return
|
||||
|
||||
const globalPos = root.mapToItem(gridLayout, 0, 0)
|
||||
const cellWidth = gridLayout.width / gridColumns
|
||||
const cellHeight = gridCellHeight + Theme.spacingS
|
||||
|
||||
const centerX = globalPos.x + (root.width / 2)
|
||||
const centerY = globalPos.y + (root.height / 2)
|
||||
|
||||
let targetCol = Math.max(0, Math.floor(centerX / cellWidth))
|
||||
let targetRow = Math.max(0, Math.floor(centerY / cellHeight))
|
||||
|
||||
targetCol = Math.min(targetCol, gridColumns - 1)
|
||||
|
||||
const newIndex = findBestInsertionIndex(targetRow, targetCol)
|
||||
|
||||
if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) {
|
||||
swapIndices(widgetIndex, newIndex)
|
||||
}
|
||||
}
|
||||
|
||||
function findBestInsertionIndex(targetRow, targetCol) {
|
||||
const widgets = SettingsData.controlCenterWidgets || [];
|
||||
const n = widgets.length;
|
||||
if (!n || widgetIndex < 0 || widgetIndex >= n) return -1;
|
||||
|
||||
function spanFor(width) {
|
||||
const w = width ?? 50;
|
||||
if (w <= 25) return 1;
|
||||
if (w <= 50) return 2;
|
||||
if (w <= 75) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
const cols = gridColumns || 4;
|
||||
|
||||
let row = 0, col = 0;
|
||||
let draggedOrigKey = null;
|
||||
|
||||
const pos = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const span = Math.min(spanFor(widgets[i].width), cols);
|
||||
|
||||
if (col + span > cols) {
|
||||
row++;
|
||||
col = 0;
|
||||
}
|
||||
|
||||
const startCol = col;
|
||||
const centerKey = row * cols + (startCol + (span - 1) / 2);
|
||||
|
||||
if (i === widgetIndex) {
|
||||
draggedOrigKey = centerKey;
|
||||
} else {
|
||||
pos.push({ index: i, row, startCol, span, centerKey });
|
||||
}
|
||||
|
||||
col += span;
|
||||
if (col >= cols) {
|
||||
row++;
|
||||
col = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos.length === 0) return -1;
|
||||
|
||||
const centerColCoord = targetCol + 0.5;
|
||||
const targetKey = targetRow * cols + centerColCoord;
|
||||
|
||||
for (let k = 0; k < pos.length; k++) {
|
||||
const p = pos[k];
|
||||
if (p.row === targetRow && centerColCoord >= p.startCol && centerColCoord < (p.startCol + p.span)) {
|
||||
return p.index;
|
||||
}
|
||||
}
|
||||
|
||||
let lo = 0, hi = pos.length - 1;
|
||||
if (targetKey <= pos[0].centerKey) return pos[0].index;
|
||||
if (targetKey >= pos[hi].centerKey) return pos[hi].index;
|
||||
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
const mk = pos[mid].centerKey;
|
||||
if (targetKey < mk) hi = mid - 1;
|
||||
else if (targetKey > mk) lo = mid + 1;
|
||||
else return pos[mid].index;
|
||||
}
|
||||
const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false;
|
||||
return (movingUp ? pos[lo].index : pos[hi].index);
|
||||
}
|
||||
|
||||
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
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: removeWidget(widgetIndex)
|
||||
}
|
||||
}
|
||||
|
||||
PieChartSizeControl {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: -6
|
||||
visible: editMode
|
||||
z: 10
|
||||
currentSize: root.widgetData?.width || 50
|
||||
isSlider: root.isSlider
|
||||
widgetIndex: root.widgetIndex
|
||||
onSizeChanged: (newSize) => {
|
||||
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||
widgets[widgetIndex].width = newSize
|
||||
SettingsData.setControlCenterWidgets(widgets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dragHandle
|
||||
width: 16
|
||||
height: 12
|
||||
radius: 2
|
||||
color: Theme.primary
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 4
|
||||
visible: editMode
|
||||
z: 15
|
||||
opacity: dragArea.drag.active ? 1.0 : 0.7
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "drag_indicator"
|
||||
size: 10
|
||||
color: Theme.primaryText
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: editMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
radius: Theme.cornerRadius
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
z: -1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
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: 0
|
||||
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: 0
|
||||
|
||||
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: 0
|
||||
|
||||
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: 0
|
||||
|
||||
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: 0
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
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: 0
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,12 @@ import qs.Common
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import qs.Modules.ControlCenter.Details
|
||||
import qs.Modules.ControlCenter.Details 1.0 as Details
|
||||
import qs.Modules.TopBar
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Components
|
||||
import qs.Modules.ControlCenter.Models
|
||||
import "./utils/state.js" as StateUtils
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
@@ -21,102 +24,94 @@ DankPopout {
|
||||
property bool powerOptionsExpanded: false
|
||||
property string triggerSection: "right"
|
||||
property var triggerScreen: null
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerX = x
|
||||
triggerY = y
|
||||
triggerWidth = width
|
||||
triggerSection = section
|
||||
triggerScreen = screen
|
||||
}
|
||||
|
||||
function openWithSection(section) {
|
||||
if (shouldBeVisible) {
|
||||
close()
|
||||
} else {
|
||||
expandedSection = section
|
||||
open()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSection(section) {
|
||||
if (expandedSection === section) {
|
||||
expandedSection = ""
|
||||
} else {
|
||||
expandedSection = section
|
||||
}
|
||||
}
|
||||
property bool editMode: false
|
||||
property int expandedWidgetIndex: -1
|
||||
property var expandedWidgetData: null
|
||||
|
||||
signal powerActionRequested(string action, string title, string message)
|
||||
signal lockRequested
|
||||
|
||||
function collapseAll() {
|
||||
expandedSection = ""
|
||||
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(Screen.height - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
|
||||
triggerX: Screen.width - 600 - Theme.spacingL
|
||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
||||
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.popupDistance
|
||||
triggerWidth: 80
|
||||
positioning: "center"
|
||||
WlrLayershell.namespace: "quickshell-controlcenter"
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: false
|
||||
visible: shouldBeVisible
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
|
||||
if (UserInfoService)
|
||||
UserInfoService.getUptime()
|
||||
Qt.callLater(() => {
|
||||
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
|
||||
if (UserInfoService)
|
||||
UserInfoService.getUptime()
|
||||
})
|
||||
} else {
|
||||
NetworkService.autoRefreshEnabled = false
|
||||
if (BluetoothService.adapter
|
||||
&& BluetoothService.adapter.discovering)
|
||||
BluetoothService.adapter.discovering = false
|
||||
Qt.callLater(() => {
|
||||
NetworkService.autoRefreshEnabled = false
|
||||
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
|
||||
BluetoothService.adapter.discovering = false
|
||||
editMode = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
WidgetModel {
|
||||
id: widgetModel
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitHeight: controlContent.implicitHeight
|
||||
Rectangle {
|
||||
id: controlContent
|
||||
|
||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
|
||||
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
||||
|
||||
Rectangle {
|
||||
id: controlContent
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
|
||||
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible)
|
||||
forceActiveFocus()
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
} else {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible)
|
||||
Qt.callLater(function () {
|
||||
controlContent.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
target: root
|
||||
}
|
||||
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
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
@@ -125,623 +120,67 @@ DankPopout {
|
||||
y: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
HeaderPane {
|
||||
id: headerPane
|
||||
width: parent.width
|
||||
height: 90
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
Theme.getContentBackgroundAlpha() * 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
id: avatarContainer
|
||||
|
||||
property bool hasImage: profileImageLoader.status === Image.Ready
|
||||
|
||||
width: 64
|
||||
height: 64
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
visible: parent.hasImage
|
||||
}
|
||||
|
||||
Image {
|
||||
id: profileImageLoader
|
||||
|
||||
source: {
|
||||
if (PortalService.profileImage === "")
|
||||
return ""
|
||||
|
||||
if (PortalService.profileImage.startsWith(
|
||||
"/"))
|
||||
return "file://" + PortalService.profileImage
|
||||
|
||||
return PortalService.profileImage
|
||||
}
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
mipmap: true
|
||||
cache: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
source: profileImageLoader
|
||||
maskEnabled: true
|
||||
maskSource: circularMask
|
||||
visible: avatarContainer.hasImage
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: circularMask
|
||||
|
||||
width: 64 - 10
|
||||
height: 64 - 10
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: Theme.primary
|
||||
visible: !parent.hasImage
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "person"
|
||||
size: Theme.iconSize + 8
|
||||
color: Theme.primaryText
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "warning"
|
||||
size: Theme.iconSize + 8
|
||||
color: Theme.primaryText
|
||||
visible: PortalService.profileImage !== ""
|
||||
&& profileImageLoader.status === Image.Error
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: UserInfoService.fullName
|
||||
|| UserInfoService.username || "User"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (UserInfoService.uptime
|
||||
|| "Unknown")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 40
|
||||
iconName: "lock"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.surfaceText
|
||||
backgroundColor: Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.5)
|
||||
hoverColor: Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b, 0.12)
|
||||
onClicked: {
|
||||
root.close()
|
||||
root.lockRequested()
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 40
|
||||
iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.surfaceText
|
||||
backgroundColor: Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.5)
|
||||
hoverColor: Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b, 0.12)
|
||||
onClicked: {
|
||||
root.powerOptionsExpanded = !root.powerOptionsExpanded
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 40
|
||||
iconName: "settings"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.surfaceText
|
||||
backgroundColor: Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.5)
|
||||
hoverColor: Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b, 0.12)
|
||||
onClicked: {
|
||||
root.close()
|
||||
settingsModal.show()
|
||||
}
|
||||
}
|
||||
powerOptionsExpanded: root.powerOptionsExpanded
|
||||
editMode: root.editMode
|
||||
onPowerOptionsExpandedChanged: root.powerOptionsExpanded = powerOptionsExpanded
|
||||
onEditModeToggled: root.editMode = !root.editMode
|
||||
onPowerActionRequested: (action, title, message) => root.powerActionRequested(action, title, message)
|
||||
onLockRequested: {
|
||||
root.close()
|
||||
root.lockRequested()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
PowerOptionsPane {
|
||||
id: powerOptionsPane
|
||||
width: parent.width
|
||||
implicitHeight: root.powerOptionsExpanded ? 60 : 0
|
||||
height: implicitHeight
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
Theme.getContentBackgroundAlpha() * 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: root.powerOptionsExpanded ? 1 : 0
|
||||
opacity: root.powerOptionsExpanded ? 1 : 0
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
visible: root.powerOptionsExpanded
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: logoutButton.containsMouse ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.5)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "logout"
|
||||
size: Theme.fontSizeSmall
|
||||
color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Logout"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
root.powerOptionsExpanded = false
|
||||
root.close()
|
||||
root.powerActionRequested(
|
||||
"logout", "Logout",
|
||||
"Are you sure you want to logout?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: rebootButton.containsMouse ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.5)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "restart_alt"
|
||||
size: Theme.fontSizeSmall
|
||||
color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Restart"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
root.powerOptionsExpanded = false
|
||||
root.close()
|
||||
root.powerActionRequested(
|
||||
"reboot", "Restart",
|
||||
"Are you sure you want to restart?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: suspendButton.containsMouse ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.5)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: Theme.fontSizeSmall
|
||||
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Suspend"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
root.powerOptionsExpanded = false
|
||||
root.close()
|
||||
root.powerActionRequested(
|
||||
"suspend", "Suspend",
|
||||
"Are you sure you want to suspend?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: shutdownButton.containsMouse ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.5)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "power_settings_new"
|
||||
size: Theme.fontSizeSmall
|
||||
color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Shutdown"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: shutdownButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
root.powerOptionsExpanded = false
|
||||
root.close()
|
||||
root.powerActionRequested(
|
||||
"poweroff", "Shutdown",
|
||||
"Are you sure you want to shutdown?")
|
||||
}
|
||||
}
|
||||
}
|
||||
expanded: root.powerOptionsExpanded
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
root.powerOptionsExpanded = false
|
||||
root.close()
|
||||
root.powerActionRequested(action, title, message)
|
||||
}
|
||||
}
|
||||
|
||||
DragDropGrid {
|
||||
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)
|
||||
}
|
||||
|
||||
Item {
|
||||
EditControls {
|
||||
width: parent.width
|
||||
height: audioSliderRow.implicitHeight
|
||||
|
||||
Row {
|
||||
id: audioSliderRow
|
||||
x: -Theme.spacingS
|
||||
width: parent.width + Theme.spacingS * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
AudioSliderRow {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
}
|
||||
|
||||
Item {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: parent.height
|
||||
|
||||
BrightnessSliderRow {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
x: -Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
NetworkPill {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
expanded: root.expandedSection === "network"
|
||||
onClicked: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return
|
||||
}
|
||||
if (NetworkService.networkStatus === "ethernet") {
|
||||
if (NetworkService.ethernetConnected && !NetworkService.wifiEnabled) {
|
||||
NetworkService.toggleWifiRadio()
|
||||
return
|
||||
}
|
||||
root.toggleSection("network")
|
||||
return
|
||||
}
|
||||
if (NetworkService.networkStatus === "wifi") {
|
||||
if (NetworkService.ethernetConnected) {
|
||||
NetworkService.toggleWifiRadio()
|
||||
return
|
||||
}
|
||||
NetworkService.disconnectWifi()
|
||||
return
|
||||
}
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
NetworkService.toggleWifiRadio()
|
||||
return
|
||||
}
|
||||
if (NetworkService.wifiEnabled && NetworkService.networkStatus === "disconnected") {
|
||||
root.toggleSection("network")
|
||||
}
|
||||
}
|
||||
onExpandClicked: root.toggleSection("network")
|
||||
}
|
||||
|
||||
BluetoothPill {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
expanded: root.expandedSection === "bluetooth"
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter)
|
||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
||||
}
|
||||
onExpandClicked: root.toggleSection("bluetooth")
|
||||
visible: BluetoothService.available
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
active: root.expandedSection === "network" || root.expandedSection === "bluetooth"
|
||||
visible: active
|
||||
sourceComponent: DetailView {
|
||||
width: parent.width
|
||||
isVisible: true
|
||||
title: {
|
||||
switch (root.expandedSection) {
|
||||
case "network": return "Network Settings"
|
||||
case "bluetooth": return "Bluetooth Settings"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
content: {
|
||||
switch (root.expandedSection) {
|
||||
case "network": return networkDetailComponent
|
||||
case "bluetooth": return bluetoothDetailComponent
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
contentHeight: 250
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
AudioOutputPill {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
expanded: root.expandedSection === "audio_output"
|
||||
onClicked: {
|
||||
if (AudioService.sink) {
|
||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||
}
|
||||
}
|
||||
onExpandClicked: root.toggleSection("audio_output")
|
||||
}
|
||||
|
||||
AudioInputPill {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
expanded: root.expandedSection === "audio_input"
|
||||
onClicked: {
|
||||
if (AudioService.source) {
|
||||
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||
}
|
||||
}
|
||||
onExpandClicked: root.toggleSection("audio_input")
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
active: root.expandedSection === "audio_output" || root.expandedSection === "audio_input"
|
||||
visible: active
|
||||
sourceComponent: DetailView {
|
||||
width: parent.width
|
||||
isVisible: true
|
||||
title: {
|
||||
switch (root.expandedSection) {
|
||||
case "audio_output": return "Audio Output"
|
||||
case "audio_input": return "Audio Input"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
content: {
|
||||
switch (root.expandedSection) {
|
||||
case "audio_output": return audioOutputDetailComponent
|
||||
case "audio_input": return audioInputDetailComponent
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
contentHeight: 250
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
ToggleButton {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
iconName: DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
|
||||
text: "Night Mode"
|
||||
secondaryText: SessionData.nightModeAutoEnabled ? "Auto" : (DisplayService.nightModeEnabled ? "On" : "Off")
|
||||
isActive: DisplayService.nightModeEnabled
|
||||
enabled: DisplayService.automationAvailable
|
||||
onClicked: DisplayService.toggleNightMode()
|
||||
|
||||
DankIcon {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
name: "schedule"
|
||||
size: 12
|
||||
color: Theme.primary
|
||||
visible: SessionData.nightModeAutoEnabled
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
iconName: SessionData.isLightMode ? "light_mode" : "palette"
|
||||
text: "Theme"
|
||||
secondaryText: SessionData.isLightMode ? "Light" : "Dark"
|
||||
isActive: true
|
||||
onClicked: Theme.toggleLightMode()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Details.BluetoothCodecSelector {
|
||||
|
||||
BluetoothCodecSelector {
|
||||
id: bluetoothCodecSelector
|
||||
anchors.fill: parent
|
||||
z: 10000
|
||||
@@ -778,4 +217,9 @@ DankPopout {
|
||||
id: audioInputDetailComponent
|
||||
AudioInputDetail {}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: batteryDetailComponent
|
||||
BatteryDetail {}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,16 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
implicitHeight: headerRow.height + volumeSlider.height + audioContent.height + Theme.spacingM
|
||||
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: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
@@ -33,7 +38,7 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
Row {
|
||||
id: volumeSlider
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -42,34 +47,60 @@ Rectangle {
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
height: 35
|
||||
value: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
leftIcon: {
|
||||
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
|
||||
let muted = AudioService.source.audio.muted
|
||||
return muted ? "mic_off" : "mic"
|
||||
}
|
||||
rightIcon: "volume_up"
|
||||
enabled: AudioService.source && AudioService.source.audio
|
||||
unit: "%"
|
||||
showValue: true
|
||||
visible: AudioService.source && AudioService.source.audio
|
||||
|
||||
onSliderValueChanged: function(newValue) {
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.volume = newValue / 100
|
||||
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"
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.left: parent.left
|
||||
|
||||
DankSlider {
|
||||
readonly property real actualVolumePercent: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.iconSize + Theme.spacingS * 2
|
||||
height: parent.height
|
||||
onClicked: {
|
||||
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.muted = !AudioService.source.audio.muted
|
||||
AudioService.source.audio.volume = newValue / 100
|
||||
if (newValue > 0 && AudioService.source.audio.muted) {
|
||||
AudioService.source.audio.muted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,12 +108,12 @@ Rectangle {
|
||||
|
||||
DankFlickable {
|
||||
id: audioContent
|
||||
anchors.top: volumeSlider.bottom
|
||||
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: Theme.spacingS
|
||||
anchors.topMargin: hasInputVolumeSliderInCC ? Theme.spacingM : Theme.spacingS
|
||||
contentHeight: audioColumn.height
|
||||
clip: true
|
||||
|
||||
@@ -103,9 +134,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
||||
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
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
@@ -163,14 +194,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
implicitHeight: headerRow.height + audioContent.height + Theme.spacingM
|
||||
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: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
@@ -32,15 +37,88 @@ Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
id: volumeSlider
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: headerRow.bottom
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
height: 35
|
||||
spacing: 0
|
||||
visible: !hasVolumeSliderInCC
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize + Theme.spacingS * 2
|
||||
height: Theme.iconSize + Theme.spacingS * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: iconArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (!AudioService.sink || !AudioService.sink.audio) return "volume_off"
|
||||
let muted = AudioService.sink.audio.muted
|
||||
let volume = AudioService.sink.audio.volume
|
||||
if (muted || volume === 0.0) return "volume_off"
|
||||
if (volume <= 0.33) return "volume_down"
|
||||
if (volume <= 0.66) return "volume_up"
|
||||
return "volume_up"
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||
enabled: AudioService.sink && AudioService.sink.audio
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0
|
||||
showValue: true
|
||||
unit: "%"
|
||||
valueOverride: actualVolumePercent
|
||||
thumbOutlineColor: Theme.surfaceVariant
|
||||
|
||||
onSliderValueChanged: function(newValue) {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.volume = newValue / 100
|
||||
if (newValue > 0 && AudioService.sink.audio.muted) {
|
||||
AudioService.sink.audio.muted = false
|
||||
}
|
||||
AudioService.volumeChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: audioContent
|
||||
anchors.top: headerRow.bottom
|
||||
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: Theme.spacingM
|
||||
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
|
||||
contentHeight: audioColumn.height
|
||||
clip: true
|
||||
|
||||
@@ -61,9 +139,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: modelData === AudioService.sink ? 2 : 1
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
@@ -123,14 +201,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: 0
|
||||
|
||||
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: 0
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
opacity: modalVisible ? 1 : 0
|
||||
scale: modalVisible ? 1 : 0.9
|
||||
|
||||
@@ -206,14 +206,14 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (modelData.name === currentCodec)
|
||||
return Theme.surfaceContainerHigh;
|
||||
return Theme.surfaceContainerHighest;
|
||||
else if (codecMouseArea.containsMouse)
|
||||
return Theme.surfaceHover;
|
||||
else
|
||||
return "transparent";
|
||||
}
|
||||
border.color: "transparent"
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
@@ -272,12 +272,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import qs.Widgets
|
||||
Rectangle {
|
||||
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
|
||||
property var bluetoothCodecModalRef: null
|
||||
|
||||
@@ -58,11 +58,11 @@ Rectangle {
|
||||
radius: 18
|
||||
color: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
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
|
||||
border.width: 0
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Row {
|
||||
@@ -96,13 +96,6 @@ Rectangle {
|
||||
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +152,7 @@ Rectangle {
|
||||
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 Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||
return Theme.surfaceContainerHighest
|
||||
}
|
||||
border.color: {
|
||||
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||
@@ -168,7 +161,7 @@ Rectangle {
|
||||
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
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
@@ -284,14 +277,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,9 +332,9 @@ Rectangle {
|
||||
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) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
|
||||
color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
opacity: canConnect ? 1 : 0.6
|
||||
|
||||
Row {
|
||||
@@ -427,9 +412,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +440,7 @@ Rectangle {
|
||||
background: Rectangle {
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
|
||||
166
Modules/ControlCenter/Details/DiskUsageDetail.qml
Normal file
166
Modules/ControlCenter/Details/DiskUsageDetail.qml
Normal file
@@ -0,0 +1,166 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
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: 0
|
||||
|
||||
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.surfaceContainerHighest
|
||||
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 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
DankIcon {
|
||||
name: "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: {
|
||||
currentMountPath = modelData.mount
|
||||
mountPathChanged(modelData.mount)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@ Rectangle {
|
||||
return headerRow.height + wifiOffContent.height + Theme.spacingM
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
NetworkService.addRef()
|
||||
@@ -27,20 +27,11 @@ Rectangle {
|
||||
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
|
||||
@@ -65,74 +56,19 @@ Rectangle {
|
||||
height: parent.height
|
||||
}
|
||||
|
||||
Row {
|
||||
DankButtonGroup {
|
||||
id: preferenceControls
|
||||
spacing: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected
|
||||
|
||||
Rectangle {
|
||||
property bool isActive: NetworkService.userPreference === "ethernet"
|
||||
|
||||
width: 90
|
||||
height: 36
|
||||
radius: 18
|
||||
color: isActive ? Theme.surfaceContainerHigh : ethernetMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Ethernet"
|
||||
color: parent.isActive ? Theme.primary : Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: parent.isActive ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ethernetMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: NetworkService.setNetworkPreference("ethernet")
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property bool isActive: NetworkService.userPreference === "wifi"
|
||||
|
||||
width: 70
|
||||
height: 36
|
||||
radius: 18
|
||||
color: isActive ? Theme.surfaceContainerHigh : wifiMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "WiFi"
|
||||
color: parent.isActive ? Theme.primary : Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: parent.isActive ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wifiMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: NetworkService.setNetworkPreference("wifi")
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,7 +149,7 @@ Rectangle {
|
||||
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.width: 0
|
||||
border.color: Theme.primary
|
||||
|
||||
StyledText {
|
||||
@@ -232,12 +168,6 @@ Rectangle {
|
||||
onClicked: NetworkService.toggleWifiRadio()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,9 +227,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
||||
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
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
@@ -310,11 +240,9 @@ Rectangle {
|
||||
DankIcon {
|
||||
name: {
|
||||
let strength = modelData.signal || 0
|
||||
if (strength >= 70) return "signal_wifi_4_bar"
|
||||
if (strength >= 50) return "network_wifi_3_bar"
|
||||
if (strength >= 25) return "network_wifi_2_bar"
|
||||
if (strength >= 10) return "network_wifi_1_bar"
|
||||
return "signal_wifi_bad"
|
||||
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
|
||||
@@ -389,9 +317,7 @@ Rectangle {
|
||||
onClicked: function(event) {
|
||||
if (modelData.ssid !== NetworkService.currentWifiSSID) {
|
||||
if (modelData.secured && !modelData.saved) {
|
||||
if (wifiPasswordModalRef) {
|
||||
wifiPasswordModalRef.show(modelData.ssid)
|
||||
}
|
||||
wifiPasswordModal.show(modelData.ssid)
|
||||
} else {
|
||||
NetworkService.connectToWifi(modelData.ssid)
|
||||
}
|
||||
@@ -400,13 +326,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,7 +345,7 @@ Rectangle {
|
||||
background: Rectangle {
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
@@ -452,9 +371,7 @@ Rectangle {
|
||||
NetworkService.disconnectWifi()
|
||||
} else {
|
||||
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
||||
if (wifiPasswordModalRef) {
|
||||
wifiPasswordModalRef.show(networkContextMenu.currentSSID)
|
||||
}
|
||||
wifiPasswordModal.show(networkContextMenu.currentSSID)
|
||||
} else {
|
||||
NetworkService.connectToWifi(networkContextMenu.currentSSID)
|
||||
}
|
||||
@@ -480,10 +397,8 @@ Rectangle {
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
if (networkInfoModalRef) {
|
||||
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
|
||||
networkInfoModalRef.showNetworkInfo(networkContextMenu.currentSSID, networkData)
|
||||
}
|
||||
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
|
||||
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,22 +426,12 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: wifiPasswordModalLoader
|
||||
active: false
|
||||
|
||||
WifiPasswordModal {
|
||||
id: wifiPasswordModal
|
||||
}
|
||||
WifiPasswordModal {
|
||||
id: wifiPasswordModal
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: networkInfoModalLoader
|
||||
active: false
|
||||
|
||||
NetworkInfoModal {
|
||||
id: networkInfoModal
|
||||
}
|
||||
|
||||
NetworkInfoModal {
|
||||
id: networkInfoModal
|
||||
}
|
||||
|
||||
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ PanelWindow {
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
opacity: powerMenuVisible ? 1 : 0
|
||||
scale: powerMenuVisible ? 1 : 0.85
|
||||
|
||||
@@ -81,8 +81,6 @@ PanelWindow {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g,
|
||||
Theme.error.b, 0.12)
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
}
|
||||
|
||||
@@ -7,17 +7,17 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
|
||||
BasePill {
|
||||
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"
|
||||
}
|
||||
@@ -41,6 +41,12 @@ BasePill {
|
||||
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
|
||||
|
||||
@@ -7,17 +7,17 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
|
||||
BasePill {
|
||||
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"
|
||||
@@ -43,6 +43,12 @@ BasePill {
|
||||
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
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
|
||||
SimpleSlider {
|
||||
id: root
|
||||
|
||||
property var defaultSink: AudioService.sink
|
||||
|
||||
iconName: {
|
||||
if (!defaultSink) return "volume_off"
|
||||
|
||||
let volume = defaultSink.audio.volume
|
||||
let muted = defaultSink.audio.muted
|
||||
|
||||
if (muted || volume === 0.0) return "volume_off"
|
||||
if (volume <= 0.33) return "volume_down"
|
||||
if (volume <= 0.66) return "volume_up"
|
||||
return "volume_up"
|
||||
}
|
||||
|
||||
iconColor: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||
|
||||
enabled: defaultSink !== null
|
||||
allowIconClick: defaultSink !== null
|
||||
|
||||
value: defaultSink ? defaultSink.audio.volume : 0.0
|
||||
maximumValue: 1.0
|
||||
minimumValue: 0.0
|
||||
|
||||
onSliderValueChanged: function(newValue) {
|
||||
if (defaultSink) {
|
||||
defaultSink.audio.volume = newValue
|
||||
if (newValue > 0 && defaultSink.audio.muted) {
|
||||
defaultSink.audio.muted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onIconClicked: function() {
|
||||
if (defaultSink) {
|
||||
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,18 @@ Row {
|
||||
id: root
|
||||
|
||||
property var defaultSink: AudioService.sink
|
||||
property color sliderTrackColor: "transparent"
|
||||
|
||||
height: 40
|
||||
spacing: Theme.spacingS
|
||||
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 // Make it circular
|
||||
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 }
|
||||
}
|
||||
@@ -42,10 +43,10 @@ Row {
|
||||
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"
|
||||
@@ -57,12 +58,19 @@ Row {
|
||||
}
|
||||
|
||||
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) - Theme.spacingM
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||
enabled: defaultSink !== null
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||
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
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
property color iconColor: Theme.surfaceText
|
||||
property string primaryText: ""
|
||||
property string secondaryText: ""
|
||||
property bool expanded: false
|
||||
property bool isActive: false
|
||||
|
||||
signal clicked()
|
||||
signal expandClicked()
|
||||
signal wheelEvent(var wheelEvent)
|
||||
|
||||
width: parent ? parent.width : 200
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
id: mainArea
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width - expandArea.width
|
||||
topLeftRadius: Theme.cornerRadius
|
||||
bottomLeftRadius: Theme.cornerRadius
|
||||
topRightRadius: 0
|
||||
bottomRightRadius: 0
|
||||
color: mainAreaMouse.containsMouse ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
"transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.iconSize
|
||||
color: root.isActive ? Theme.primary : root.iconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - Theme.iconSize - Theme.spacingS
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.primaryText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.secondaryText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: text.length > 0
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mainAreaMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
onWheel: function (wheelEvent) {
|
||||
root.wheelEvent(wheelEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: expandArea
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: Theme.iconSize + Theme.spacingM * 2
|
||||
topLeftRadius: 0
|
||||
bottomLeftRadius: 0
|
||||
topRightRadius: Theme.cornerRadius
|
||||
bottomRightRadius: Theme.cornerRadius
|
||||
color: expandAreaMouse.containsMouse ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
"transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: expandIcon
|
||||
anchors.centerIn: parent
|
||||
name: expanded ? "expand_less" : "expand_more"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: expandAreaMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expandClicked()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,14 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
|
||||
BasePill {
|
||||
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) {
|
||||
@@ -30,17 +30,15 @@ BasePill {
|
||||
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 unavailable"
|
||||
return "Bluetooth"
|
||||
}
|
||||
if (!BluetoothService.adapter) {
|
||||
return "No adapter"
|
||||
@@ -53,7 +51,7 @@ BasePill {
|
||||
|
||||
secondaryText: {
|
||||
if (!BluetoothService.available) {
|
||||
return "Hardware not found"
|
||||
return "No adapters"
|
||||
}
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||
return "Off"
|
||||
@@ -63,4 +61,10 @@ BasePill {
|
||||
}
|
||||
return "No devices"
|
||||
}
|
||||
|
||||
onToggled: {
|
||||
if (BluetoothService.available && BluetoothService.adapter) {
|
||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
|
||||
SimpleSlider {
|
||||
id: root
|
||||
|
||||
iconName: {
|
||||
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
||||
|
||||
let brightness = DisplayService.brightnessLevel
|
||||
if (brightness <= 33) return "brightness_low"
|
||||
if (brightness <= 66) return "brightness_medium"
|
||||
return "brightness_high"
|
||||
}
|
||||
|
||||
iconColor: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||
|
||||
enabled: DisplayService.brightnessAvailable
|
||||
|
||||
value: DisplayService.brightnessLevel
|
||||
maximumValue: 100.0
|
||||
minimumValue: 0.0
|
||||
|
||||
onSliderValueChanged: function(newValue) {
|
||||
if (DisplayService.brightnessAvailable) {
|
||||
DisplayService.brightnessLevel = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ Row {
|
||||
id: root
|
||||
|
||||
height: 40
|
||||
spacing: Theme.spacingS
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize + Theme.spacingS * 2
|
||||
@@ -17,9 +17,9 @@ Row {
|
||||
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)
|
||||
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
: "transparent"
|
||||
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
@@ -29,7 +29,7 @@ Row {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
|
||||
onClicked: function(event) {
|
||||
if (DisplayService.devices.length > 1) {
|
||||
if (deviceMenu.visible) {
|
||||
@@ -40,26 +40,26 @@ Row {
|
||||
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"
|
||||
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
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||
enabled: DisplayService.brightnessAvailable
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
@@ -79,6 +79,8 @@ Row {
|
||||
DisplayService.setBrightness(newValue)
|
||||
}
|
||||
}
|
||||
thumbOutlineColor: Theme.surfaceContainer
|
||||
trackColor: Theme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
Menu {
|
||||
@@ -89,7 +91,7 @@ Row {
|
||||
background: Rectangle {
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ Rectangle {
|
||||
width: parent ? parent.width : 200
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
border.width: 0
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
|
||||
Row {
|
||||
|
||||
170
Modules/ControlCenter/Widgets/CompoundPill.qml
Normal file
170
Modules/ControlCenter/Widgets/CompoundPill.qml
Normal file
@@ -0,0 +1,170 @@
|
||||
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: {
|
||||
const baseColor = bodyMouse.containsMouse ? Theme.widgetBaseHoverColor : _containerBg
|
||||
return baseColor
|
||||
}
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.10)
|
||||
border.width: 0
|
||||
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 }
|
||||
}
|
||||
}
|
||||
78
Modules/ControlCenter/Widgets/DiskUsagePill.qml
Normal file
78
Modules/ControlCenter/Widgets/DiskUsagePill.qml
Normal file
@@ -0,0 +1,78 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
|
||||
CompoundPill {
|
||||
id: root
|
||||
|
||||
property string mountPath: "/"
|
||||
property string instanceId: ""
|
||||
|
||||
iconName: "storage"
|
||||
|
||||
property var selectedMount: {
|
||||
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const targetMount = DgopService.diskMounts.find(mount => mount.mount === mountPath)
|
||||
return targetMount || DgopService.diskMounts.find(mount => mount.mount === "/") || DgopService.diskMounts[0]
|
||||
}
|
||||
|
||||
property real usagePercent: {
|
||||
if (!selectedMount || !selectedMount.percent) {
|
||||
return 0
|
||||
}
|
||||
const percentStr = selectedMount.percent.replace("%", "")
|
||||
return parseFloat(percentStr) || 0
|
||||
}
|
||||
|
||||
isActive: DgopService.dgopAvailable && selectedMount !== null
|
||||
|
||||
primaryText: {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
return "Disk Usage"
|
||||
}
|
||||
if (!selectedMount) {
|
||||
return "No disk data"
|
||||
}
|
||||
return selectedMount.mount
|
||||
}
|
||||
|
||||
secondaryText: {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
return "dgop not available"
|
||||
}
|
||||
if (!selectedMount) {
|
||||
return "No disk data available"
|
||||
}
|
||||
return `${selectedMount.used} / ${selectedMount.size} (${usagePercent.toFixed(0)}%)`
|
||||
}
|
||||
|
||||
iconColor: {
|
||||
if (!DgopService.dgopAvailable || !selectedMount) {
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
}
|
||||
if (usagePercent > 90) {
|
||||
return Theme.error
|
||||
}
|
||||
if (usagePercent > 75) {
|
||||
return Theme.warning
|
||||
}
|
||||
return Theme.surfaceText
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["diskmounts"])
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["diskmounts"])
|
||||
}
|
||||
|
||||
onToggled: {
|
||||
expandClicked()
|
||||
}
|
||||
}
|
||||
81
Modules/ControlCenter/Widgets/InputAudioSliderRow.qml
Normal file
81
Modules/ControlCenter/Widgets/InputAudioSliderRow.qml
Normal file
@@ -0,0 +1,81 @@
|
||||
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 defaultSource: AudioService.source
|
||||
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: defaultSource !== null
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (defaultSource) {
|
||||
defaultSource.audio.muted = !defaultSource.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
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"
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||
enabled: defaultSource !== null
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
value: defaultSource ? Math.min(100, Math.round(defaultSource.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 (defaultSource) {
|
||||
defaultSource.audio.volume = newValue / 100.0
|
||||
if (newValue > 0 && defaultSource.audio.muted) {
|
||||
defaultSource.audio.muted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
|
||||
BasePill {
|
||||
CompoundPill {
|
||||
id: root
|
||||
|
||||
isActive: {
|
||||
@@ -21,7 +21,7 @@ BasePill {
|
||||
}
|
||||
return NetworkService.wifiEnabled
|
||||
}
|
||||
|
||||
|
||||
iconName: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return "sync"
|
||||
@@ -33,7 +33,7 @@ BasePill {
|
||||
return NetworkService.wifiSignalIcon
|
||||
}
|
||||
if (NetworkService.wifiEnabled) {
|
||||
return "signal_wifi_off"
|
||||
return "wifi_off"
|
||||
}
|
||||
return "wifi_off"
|
||||
}
|
||||
@@ -67,6 +67,12 @@ BasePill {
|
||||
if (NetworkService.wifiEnabled) {
|
||||
return "Select network"
|
||||
}
|
||||
return "Tap to enable"
|
||||
return ""
|
||||
}
|
||||
|
||||
onToggled: {
|
||||
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
|
||||
NetworkService.toggleWifiRadio()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
property color iconColor: Theme.surfaceText
|
||||
property real value: 0.0
|
||||
property real maximumValue: 1.0
|
||||
property real minimumValue: 0.0
|
||||
property bool enabled: true
|
||||
property bool allowIconClick: false
|
||||
|
||||
signal sliderValueChanged(real value)
|
||||
signal iconClicked()
|
||||
|
||||
height: 60
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.iconSize
|
||||
color: root.iconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: root.allowIconClick
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.iconClicked()
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: {
|
||||
if (parent.width <= 0) return 80
|
||||
return Math.max(80, Math.min(400, parent.width - Theme.iconSize - Theme.spacingM))
|
||||
}
|
||||
enabled: root.enabled
|
||||
minimum: Math.round(root.minimumValue * 100)
|
||||
maximum: Math.round(root.maximumValue * 100)
|
||||
value: Math.round(root.value * 100)
|
||||
onSliderValueChanged: function(newValue) { root.sliderValueChanged(newValue / 100.0) }
|
||||
}
|
||||
}
|
||||
107
Modules/ControlCenter/Widgets/SmallBatteryButton.qml
Normal file
107
Modules/ControlCenter/Widgets/SmallBatteryButton.qml
Normal file
@@ -0,0 +1,107 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
|
||||
property bool enabled: BatteryService.batteryAvailable
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
|
||||
height: 48
|
||||
radius: {
|
||||
if (Theme.cornerRadius === 0) return 0
|
||||
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4
|
||||
}
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
||||
}
|
||||
|
||||
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)
|
||||
readonly property color _tileIconActive: Theme.primaryContainer
|
||||
readonly property color _tileIconInactive: Theme.primary
|
||||
|
||||
color: {
|
||||
if (isActive) return _tileBgActive
|
||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
|
||||
return baseColor
|
||||
}
|
||||
border.color: isActive ? _tileRingActive : "transparent"
|
||||
border.width: isActive ? 1 : 0
|
||||
antialiasing: true
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: hoverTint(root.color)
|
||||
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } }
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
DankIcon {
|
||||
name: BatteryService.getBatteryIcon()
|
||||
size: parent.parent.width * 0.25
|
||||
color: {
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||
return Theme.error
|
||||
}
|
||||
return isActive ? _tileIconActive : _tileIconInactive
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : ""
|
||||
font.pixelSize: parent.parent.width * 0.15
|
||||
font.weight: Font.Medium
|
||||
color: {
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||
return Theme.error
|
||||
}
|
||||
return isActive ? _tileIconActive : _tileIconInactive
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: BatteryService.batteryAvailable
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Modules/ControlCenter/Widgets/SmallToggleButton.qml
Normal file
85
Modules/ControlCenter/Widgets/SmallToggleButton.qml
Normal file
@@ -0,0 +1,85 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
property bool isActive: false
|
||||
property bool enabled: true
|
||||
property real iconRotation: 0
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
|
||||
height: 48
|
||||
radius: {
|
||||
if (Theme.cornerRadius === 0) return 0
|
||||
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4
|
||||
}
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
||||
}
|
||||
|
||||
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)
|
||||
readonly property color _tileIconActive: Theme.primaryContainer
|
||||
readonly property color _tileIconInactive: Theme.primary
|
||||
|
||||
color: {
|
||||
if (isActive) return _tileBgActive
|
||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
|
||||
return baseColor
|
||||
}
|
||||
border.color: isActive ? _tileRingActive : "transparent"
|
||||
border.width: isActive ? 1 : 0
|
||||
antialiasing: true
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: hoverTint(root.color)
|
||||
opacity: mouseArea.pressed ? 0.3 : (mouseArea.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
|
||||
rotation: iconRotation
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
@@ -12,67 +11,92 @@ Rectangle {
|
||||
property bool isActive: false
|
||||
property bool enabled: true
|
||||
property string secondaryText: ""
|
||||
property real iconRotation: 0
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: parent ? parent.width : 200
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
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: {
|
||||
if (isActive) return _tileBgActive
|
||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
|
||||
return baseColor
|
||||
}
|
||||
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
||||
}
|
||||
|
||||
readonly property color _containerBg: Theme.surfaceContainerHigh
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
"transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
color: mouseArea.containsMouse ? hoverTint(_containerBg) : "transparent"
|
||||
opacity: mouseArea.containsMouse ? 0.08 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL + 2
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.iconSize
|
||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||
color: isActive ? Theme.primaryContainer : Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
rotation: root.iconRotation
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - Theme.iconSize - Theme.spacingS
|
||||
spacing: 2
|
||||
Item {
|
||||
width: parent.width - Theme.iconSize - parent.spacing
|
||||
height: parent.height
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.secondaryText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: text.length > 0
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: isActive ? Theme.primaryContainer : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.secondaryText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: isActive ? Theme.primaryContainer : Theme.surfaceVariantText
|
||||
visible: text.length > 0
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,4 +116,11 @@ Rectangle {
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Modules/ControlCenter/utils/layout.js
Normal file
45
Modules/ControlCenter/utils/layout.js
Normal file
@@ -0,0 +1,45 @@
|
||||
function calculateRowsAndWidgets(controlCenterColumn, expandedSection, expandedWidgetIndex) {
|
||||
var rows = []
|
||||
var currentRow = []
|
||||
var currentWidth = 0
|
||||
var expandedRow = -1
|
||||
|
||||
const widgets = SettingsData.controlCenterWidgets || []
|
||||
const baseWidth = controlCenterColumn.width
|
||||
const spacing = Theme.spacingS
|
||||
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
const widget = widgets[i]
|
||||
const widgetWidth = widget.width || 50
|
||||
|
||||
var itemWidth
|
||||
if (widgetWidth <= 25) {
|
||||
itemWidth = (baseWidth - spacing * 3) / 4
|
||||
} else if (widgetWidth <= 50) {
|
||||
itemWidth = (baseWidth - spacing) / 2
|
||||
} else if (widgetWidth <= 75) {
|
||||
itemWidth = (baseWidth - spacing * 2) * 0.75
|
||||
} else {
|
||||
itemWidth = baseWidth
|
||||
}
|
||||
|
||||
if (currentRow.length > 0 && (currentWidth + spacing + itemWidth > baseWidth)) {
|
||||
rows.push([...currentRow])
|
||||
currentRow = [widget]
|
||||
currentWidth = itemWidth
|
||||
} else {
|
||||
currentRow.push(widget)
|
||||
currentWidth += (currentRow.length > 1 ? spacing : 0) + itemWidth
|
||||
}
|
||||
|
||||
if (expandedWidgetIndex === i) {
|
||||
expandedRow = rows.length
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRow.length > 0) {
|
||||
rows.push(currentRow)
|
||||
}
|
||||
|
||||
return { rows: rows, expandedRowIndex: expandedRow }
|
||||
}
|
||||
25
Modules/ControlCenter/utils/state.js
Normal file
25
Modules/ControlCenter/utils/state.js
Normal file
@@ -0,0 +1,25 @@
|
||||
function setTriggerPosition(root, x, y, width, section, screen) {
|
||||
root.triggerX = x
|
||||
root.triggerY = y
|
||||
root.triggerWidth = width
|
||||
root.triggerSection = section
|
||||
root.triggerScreen = screen
|
||||
}
|
||||
|
||||
function openWithSection(root, section) {
|
||||
if (root.shouldBeVisible) {
|
||||
root.close()
|
||||
} else {
|
||||
root.expandedSection = section
|
||||
root.open()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSection(root, section) {
|
||||
if (root.expandedSection === section) {
|
||||
root.expandedSection = ""
|
||||
root.expandedWidgetIndex = -1
|
||||
} else {
|
||||
root.expandedSection = section
|
||||
}
|
||||
}
|
||||
85
Modules/ControlCenter/utils/widgets.js
Normal file
85
Modules/ControlCenter/utils/widgets.js
Normal file
@@ -0,0 +1,85 @@
|
||||
function getWidgetForId(baseWidgetDefinitions, widgetId) {
|
||||
return baseWidgetDefinitions.find(w => w.id === widgetId)
|
||||
}
|
||||
|
||||
function addWidget(widgetId) {
|
||||
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||
var widget = {
|
||||
"id": widgetId,
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}
|
||||
|
||||
if (widgetId === "diskUsage") {
|
||||
widget.instanceId = generateUniqueId()
|
||||
widget.mountPath = "/"
|
||||
}
|
||||
|
||||
widgets.push(widget)
|
||||
SettingsData.setControlCenterWidgets(widgets)
|
||||
}
|
||||
|
||||
function generateUniqueId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
function removeWidget(index) {
|
||||
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||
if (index >= 0 && index < widgets.length) {
|
||||
widgets.splice(index, 1)
|
||||
SettingsData.setControlCenterWidgets(widgets)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleWidgetSize(index) {
|
||||
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||
if (index >= 0 && index < widgets.length) {
|
||||
const currentWidth = widgets[index].width || 50
|
||||
const id = widgets[index].id || ""
|
||||
|
||||
if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
|
||||
widgets[index].width = currentWidth <= 50 ? 100 : 50
|
||||
} else {
|
||||
if (currentWidth <= 25) {
|
||||
widgets[index].width = 50
|
||||
} else if (currentWidth <= 50) {
|
||||
widgets[index].width = 100
|
||||
} else {
|
||||
widgets[index].width = 25
|
||||
}
|
||||
}
|
||||
|
||||
SettingsData.setControlCenterWidgets(widgets)
|
||||
}
|
||||
}
|
||||
|
||||
function reorderWidgets(newOrder) {
|
||||
SettingsData.setControlCenterWidgets(newOrder)
|
||||
}
|
||||
|
||||
function moveWidget(fromIndex, toIndex) {
|
||||
let widgets = [...(SettingsData.controlCenterWidgets || [])]
|
||||
if (fromIndex >= 0 && fromIndex < widgets.length && toIndex >= 0 && toIndex < widgets.length) {
|
||||
const movedWidget = widgets.splice(fromIndex, 1)[0]
|
||||
widgets.splice(toIndex, 0, movedWidget)
|
||||
SettingsData.setControlCenterWidgets(widgets)
|
||||
}
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
const defaultWidgets = [
|
||||
{"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}
|
||||
]
|
||||
SettingsData.setControlCenterWidgets(defaultWidgets)
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
SettingsData.setControlCenterWidgets([])
|
||||
}
|
||||
201
Modules/DankDash/DankDashPopout.qml
Normal file
201
Modules/DankDash/DankDashPopout.qml
Normal file
@@ -0,0 +1,201 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.DankDash
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
property bool dashVisible: false
|
||||
property string triggerSection: "center"
|
||||
property var triggerScreen: null
|
||||
property int currentTabIndex: 0
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
if (section === "center") {
|
||||
const screenWidth = screen ? screen.width : Screen.width
|
||||
triggerX = (screenWidth - popupWidth) / 2
|
||||
triggerWidth = popupWidth
|
||||
} else {
|
||||
triggerX = x
|
||||
triggerWidth = width
|
||||
}
|
||||
triggerY = y
|
||||
triggerSection = section
|
||||
triggerScreen = screen
|
||||
}
|
||||
|
||||
popupWidth: 700
|
||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||
triggerX: Screen.width - 620 - Theme.spacingL
|
||||
triggerY: Math.max(26 + SettingsData.topBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.topBarInnerPadding)) + SettingsData.topBarSpacing + SettingsData.topBarBottomGap - 2 + Theme.popupDistance
|
||||
triggerWidth: 80
|
||||
positioning: "center"
|
||||
shouldBeVisible: dashVisible
|
||||
visible: shouldBeVisible
|
||||
|
||||
|
||||
onDashVisibleChanged: {
|
||||
if (dashVisible) {
|
||||
open()
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
onBackgroundClicked: {
|
||||
dashVisible = false
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: mainContainer
|
||||
|
||||
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible) {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.dashVisible = false
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
Qt.callLater(function() {
|
||||
mainContainer.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
target: root
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root.shouldBeVisible
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.08
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.02
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankTabBar {
|
||||
id: tabBar
|
||||
|
||||
width: parent.width
|
||||
height: 48
|
||||
currentIndex: root.currentTabIndex
|
||||
spacing: Theme.spacingS
|
||||
equalWidthTabs: true
|
||||
|
||||
model: {
|
||||
let tabs = [
|
||||
{ icon: "dashboard", text: "Overview" },
|
||||
{ icon: "music_note", text: "Media" }
|
||||
]
|
||||
|
||||
if (SettingsData.weatherEnabled) {
|
||||
tabs.push({ icon: "wb_sunny", text: "Weather" })
|
||||
}
|
||||
|
||||
tabs.push({ icon: "settings", text: "Settings", isAction: true })
|
||||
return tabs
|
||||
}
|
||||
|
||||
onTabClicked: function(index) {
|
||||
root.currentTabIndex = index
|
||||
}
|
||||
|
||||
onActionTriggered: function(index) {
|
||||
let settingsIndex = SettingsData.weatherEnabled ? 3 : 2
|
||||
if (index === settingsIndex) {
|
||||
dashVisible = false
|
||||
settingsModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.spacingXS
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: pages
|
||||
width: parent.width
|
||||
implicitHeight: {
|
||||
if (currentIndex === 0) return overviewTab.implicitHeight
|
||||
if (currentIndex === 1) return mediaTab.implicitHeight
|
||||
if (SettingsData.weatherEnabled && currentIndex === 2) return weatherTab.implicitHeight
|
||||
return overviewTab.implicitHeight
|
||||
}
|
||||
currentIndex: root.currentTabIndex
|
||||
|
||||
OverviewTab {
|
||||
id: overviewTab
|
||||
|
||||
onSwitchToWeatherTab: {
|
||||
if (SettingsData.weatherEnabled) {
|
||||
tabBar.currentIndex = 2
|
||||
tabBar.tabClicked(2)
|
||||
}
|
||||
}
|
||||
|
||||
onSwitchToMediaTab: {
|
||||
tabBar.currentIndex = 1
|
||||
tabBar.tabClicked(1)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayerTab {
|
||||
id: mediaTab
|
||||
}
|
||||
|
||||
WeatherTab {
|
||||
id: weatherTab
|
||||
visible: SettingsData.weatherEnabled && root.currentTabIndex === 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1312
Modules/DankDash/MediaPlayerTab.qml
Normal file
1312
Modules/DankDash/MediaPlayerTab.qml
Normal file
File diff suppressed because it is too large
Load Diff
450
Modules/DankDash/Overview/CalendarOverviewCard.qml
Normal file
450
Modules/DankDash/Overview/CalendarOverviewCard.qml
Normal file
@@ -0,0 +1,450 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool showEventDetails: false
|
||||
property date selectedDate: systemClock.date
|
||||
property var selectedDateEvents: []
|
||||
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
|
||||
|
||||
function weekStartJs() {
|
||||
return Qt.locale().firstDayOfWeek % 7
|
||||
}
|
||||
|
||||
function startOfWeek(dateObj) {
|
||||
const d = new Date(dateObj)
|
||||
const jsDow = d.getDay()
|
||||
const diff = (jsDow - weekStartJs() + 7) % 7
|
||||
d.setDate(d.getDate() - diff)
|
||||
return d
|
||||
}
|
||||
|
||||
function endOfWeek(dateObj) {
|
||||
const d = new Date(dateObj)
|
||||
const jsDow = d.getDay()
|
||||
const add = (weekStartJs() + 6 - jsDow + 7) % 7
|
||||
d.setDate(d.getDate() + add)
|
||||
return d
|
||||
}
|
||||
|
||||
function updateSelectedDateEvents() {
|
||||
if (CalendarService && CalendarService.khalAvailable) {
|
||||
const events = CalendarService.getEventsForDate(selectedDate)
|
||||
selectedDateEvents = events
|
||||
} else {
|
||||
selectedDateEvents = []
|
||||
}
|
||||
}
|
||||
|
||||
function loadEventsForMonth() {
|
||||
if (!CalendarService || !CalendarService.khalAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
const firstOfMonth = new Date(calendarGrid.displayDate.getFullYear(),
|
||||
calendarGrid.displayDate.getMonth(), 1)
|
||||
const lastOfMonth = new Date(calendarGrid.displayDate.getFullYear(),
|
||||
calendarGrid.displayDate.getMonth() + 1, 0)
|
||||
|
||||
const startDate = startOfWeek(firstOfMonth)
|
||||
startDate.setDate(startDate.getDate() - 7)
|
||||
|
||||
const endDate = endOfWeek(lastOfMonth)
|
||||
endDate.setDate(endDate.getDate() + 7)
|
||||
|
||||
CalendarService.loadEvents(startDate, endDate)
|
||||
}
|
||||
|
||||
onSelectedDateChanged: updateSelectedDateEvents()
|
||||
Component.onCompleted: {
|
||||
loadEventsForMonth()
|
||||
updateSelectedDateEvents()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onEventsByDateChanged() {
|
||||
updateSelectedDateEvents()
|
||||
}
|
||||
|
||||
function onKhalAvailableChanged() {
|
||||
if (CalendarService && CalendarService.khalAvailable) {
|
||||
loadEventsForMonth()
|
||||
}
|
||||
updateSelectedDateEvents()
|
||||
}
|
||||
|
||||
target: CalendarService
|
||||
enabled: CalendarService !== null
|
||||
}
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
visible: showEventDetails
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
radius: Theme.cornerRadius
|
||||
color: backButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: 14
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.showEventDetails = false
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 32 + Theme.spacingS * 2
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
height: 40
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
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
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 28
|
||||
visible: !showEventDetails
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
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: 14
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevMonthArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let newDate = new Date(calendarGrid.displayDate)
|
||||
newDate.setMonth(newDate.getMonth() - 1)
|
||||
calendarGrid.displayDate = newDate
|
||||
loadEventsForMonth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 56
|
||||
height: 28
|
||||
text: calendarGrid.displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
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: 14
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextMonthArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let newDate = new Date(calendarGrid.displayDate)
|
||||
newDate.setMonth(newDate.getMonth() + 1)
|
||||
calendarGrid.displayDate = newDate
|
||||
loadEventsForMonth()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 18
|
||||
visible: !showEventDetails
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const days = []
|
||||
const loc = Qt.locale()
|
||||
const qtFirst = loc.firstDayOfWeek
|
||||
for (let i = 0; i < 7; ++i) {
|
||||
const qtDay = ((qtFirst - 1 + i) % 7) + 1
|
||||
days.push(loc.dayName(qtDay, Locale.ShortFormat))
|
||||
}
|
||||
return days
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 7
|
||||
height: 18
|
||||
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 {
|
||||
id: calendarGrid
|
||||
visible: !showEventDetails
|
||||
|
||||
property date displayDate: systemClock.date
|
||||
property date selectedDate: systemClock.date
|
||||
|
||||
readonly property date firstDay: {
|
||||
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
|
||||
return startOfWeek(firstOfMonth)
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - 28 - 18 - Theme.spacingS * 2
|
||||
columns: 7
|
||||
rows: 6
|
||||
|
||||
Repeater {
|
||||
model: 42
|
||||
|
||||
Rectangle {
|
||||
readonly property date dayDate: {
|
||||
const date = new Date(parent.firstDay)
|
||||
date.setDate(date.getDate() + index)
|
||||
return date
|
||||
}
|
||||
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
|
||||
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
|
||||
|
||||
width: parent.width / 7
|
||||
height: parent.height / 6
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width - 4, parent.height - 4, 32)
|
||||
height: width
|
||||
color: 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: width / 2
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: dayDate.getDate()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
font.weight: isToday ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottomMargin: 4
|
||||
width: 12
|
||||
height: 2
|
||||
radius: 1
|
||||
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
|
||||
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
|
||||
opacity: isToday ? 0.9 : 0.7
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dayArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
|
||||
root.selectedDate = dayDate
|
||||
root.showEventDetails = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankListView {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
model: selectedDateEvents
|
||||
visible: showEventDetails
|
||||
clip: true
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
delegate: Rectangle {
|
||||
width: parent ? parent.width : 0
|
||||
height: eventContent.implicitHeight + Theme.spacingS
|
||||
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 Theme.surfaceContainerHigh
|
||||
}
|
||||
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: 3
|
||||
height: parent.height - 6
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 3
|
||||
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.spacingS + 6
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData.title
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (!modelData || modelData.allDay) {
|
||||
return "All day"
|
||||
} else if (modelData.start && modelData.end) {
|
||||
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
||||
const 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
|
||||
}
|
||||
return ""
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
font.weight: Font.Normal
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
precision: SystemClock.Hours
|
||||
}
|
||||
}
|
||||
22
Modules/DankDash/Overview/Card.qml
Normal file
22
Modules/DankDash/Overview/Card.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
|
||||
property int pad: 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
|
||||
|
||||
default property alias content: contentItem.data
|
||||
|
||||
Item {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
anchors.margins: card.pad
|
||||
}
|
||||
}
|
||||
93
Modules/DankDash/Overview/ClockCard.qml
Normal file
93
Modules/DankDash/Overview/ClockCard.qml
Normal file
@@ -0,0 +1,93 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Card {
|
||||
id: root
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
spacing: -8
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (SettingsData.use24HourClock) {
|
||||
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0)
|
||||
} else {
|
||||
const hours = systemClock?.date?.getHours()
|
||||
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
|
||||
return String(display).padStart(2, '0').charAt(0)
|
||||
}
|
||||
}
|
||||
font.pixelSize: 48
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: 28
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (SettingsData.use24HourClock) {
|
||||
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1)
|
||||
} else {
|
||||
const hours = systemClock?.date?.getHours()
|
||||
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
|
||||
return String(display).padStart(2, '0').charAt(1)
|
||||
}
|
||||
}
|
||||
font.pixelSize: 48
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: 28
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
|
||||
font.pixelSize: 48
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: 28
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
|
||||
font.pixelSize: 48
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: 28
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM dd")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
}
|
||||
218
Modules/DankDash/Overview/MediaOverviewCard.qml
Normal file
218
Modules/DankDash/Overview/MediaOverviewCard.qml
Normal file
@@ -0,0 +1,218 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Card {
|
||||
id: root
|
||||
clip: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
property real currentPosition: activePlayer?.positionSupported ? activePlayer.position : 0
|
||||
property real displayPosition: currentPosition
|
||||
|
||||
readonly property real ratio: {
|
||||
if (!activePlayer || activePlayer.length <= 0) return 0
|
||||
const pos = displayPosition % Math.max(1, activePlayer.length)
|
||||
const calculatedRatio = pos / activePlayer.length
|
||||
return Math.max(0, Math.min(1, calculatedRatio))
|
||||
}
|
||||
|
||||
onActivePlayerChanged: {
|
||||
if (activePlayer?.positionSupported) {
|
||||
currentPosition = Qt.binding(() => activePlayer?.position || 0)
|
||||
} else {
|
||||
currentPosition = 0
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 300
|
||||
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
|
||||
repeat: true
|
||||
onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
|
||||
}
|
||||
|
||||
property bool isSeeking: false
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: !activePlayer
|
||||
|
||||
DankIcon {
|
||||
name: "music_note"
|
||||
size: Theme.iconSize
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No Media"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingXS * 2
|
||||
spacing: Theme.spacingL
|
||||
visible: activePlayer
|
||||
|
||||
Item {
|
||||
width: 140
|
||||
height: 110
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
clip: false
|
||||
|
||||
DankAlbumArt {
|
||||
width: 110
|
||||
height: 80
|
||||
anchors.centerIn: parent
|
||||
activePlayer: root.activePlayer
|
||||
albumSize: 76
|
||||
animationScale: 1.05
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
topPadding: Theme.spacingL
|
||||
|
||||
StyledText {
|
||||
text: activePlayer?.trackTitle || "Unknown"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: activePlayer?.trackArtist || "Unknown Artist"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankSeekbar {
|
||||
width: parent.width + 4
|
||||
height: 20
|
||||
x: -2
|
||||
activePlayer: root.activePlayer
|
||||
isSeeking: root.isSeeking
|
||||
onIsSeekingChanged: root.isSeeking = isSeeking
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
anchors.centerIn: parent
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
anchors.verticalCenter: playPauseButton.verticalCenter
|
||||
color: prevArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_previous"
|
||||
size: 14
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevArea
|
||||
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 {
|
||||
id: playPauseButton
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Theme.primary
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
size: 16
|
||||
color: Theme.background
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: activePlayer?.togglePlaying()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
anchors.verticalCenter: playPauseButton.verticalCenter
|
||||
color: nextArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_next"
|
||||
size: 14
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: activePlayer?.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 123
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
visible: activePlayer
|
||||
}
|
||||
}
|
||||
178
Modules/DankDash/Overview/SystemMonitorCard.qml
Normal file
178
Modules/DankDash/Overview/SystemMonitorCard.qml
Normal file
@@ -0,0 +1,178 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Card {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["cpu", "memory", "system"])
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["cpu", "memory", "system"])
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// CPU Bar
|
||||
Column {
|
||||
width: (parent.width - 2 * Theme.spacingM) / 3
|
||||
height: parent.height
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: 8
|
||||
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
|
||||
radius: 4
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height * Math.min((DgopService.cpuUsage || 6) / 100, 1)
|
||||
radius: parent.radius
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: {
|
||||
if (DgopService.cpuUsage > 80) return Theme.error
|
||||
if (DgopService.cpuUsage > 60) return Theme.warning
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.iconSizeSmall
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: Theme.iconSizeSmall
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
color: {
|
||||
if (DgopService.cpuUsage > 80) return Theme.error
|
||||
if (DgopService.cpuUsage > 60) return Theme.warning
|
||||
return Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature Bar
|
||||
Column {
|
||||
width: (parent.width - 2 * Theme.spacingM) / 3
|
||||
height: parent.height
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: 8
|
||||
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
|
||||
radius: 4
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height * Math.min(Math.max((DgopService.cpuTemperature || 40) / 100, 0), 1)
|
||||
radius: parent.radius
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: {
|
||||
if (DgopService.cpuTemperature > 85) return Theme.error
|
||||
if (DgopService.cpuTemperature > 69) return Theme.warning
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.iconSizeSmall
|
||||
|
||||
DankIcon {
|
||||
name: "device_thermostat"
|
||||
size: Theme.iconSizeSmall
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
color: {
|
||||
if (DgopService.cpuTemperature > 85) return Theme.error
|
||||
if (DgopService.cpuTemperature > 69) return Theme.warning
|
||||
return Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RAM Bar
|
||||
Column {
|
||||
width: (parent.width - 2 * Theme.spacingM) / 3
|
||||
height: parent.height
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: 8
|
||||
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
|
||||
radius: 4
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height * Math.min((DgopService.memoryUsage || 42) / 100, 1)
|
||||
radius: parent.radius
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: {
|
||||
if (DgopService.memoryUsage > 90) return Theme.error
|
||||
if (DgopService.memoryUsage > 75) return Theme.warning
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.iconSizeSmall
|
||||
|
||||
DankIcon {
|
||||
name: "developer_board"
|
||||
size: Theme.iconSizeSmall
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
color: {
|
||||
if (DgopService.memoryUsage > 90) return Theme.error
|
||||
if (DgopService.memoryUsage > 75) return Theme.warning
|
||||
return Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user