1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

Compare commits

...

108 Commits

Author SHA1 Message Date
github-actions[bot]
b27f362b44 Update VERSION to v0.3.0 (from DMS) 2025-10-30 18:11:33 +00:00
bbedward
325e3bc19b fix duplicated qt6ct sections 2025-10-30 13:53:47 -04:00
bbedward
9215985335 ci: try and fix changelog filter 2025-10-30 13:45:39 -04:00
Mattias
293179daa6 fix: Enable "Show on Last Display" for Notepad Slideout and System Tray (#590) 2025-10-30 13:38:57 -04:00
github-actions[bot]
4fe79dbe85 i18n: update source strings from codebase 2025-10-30 17:14:49 +00:00
bbedward
55d738e917 about: fix links 2025-10-30 13:13:29 -04:00
github-actions[bot]
986b07f4a9 i18n: update translations 2025-10-30 17:04:57 +00:00
github-actions[bot]
450c2e91ed i18n: update source strings from codebase 2025-10-30 17:04:47 +00:00
bbedward
4d06333624 about page: update for mango and sway 2025-10-30 13:04:10 -04:00
Tulip Blossom
fbe4122404 fix(dms-greeter,rpm): greeter user is supplied by sysusers and having manual user on the spec breaks it (#585)
* fix(dms-greeter,rpm): greeter user is supplied by sysusers and having manual user on the spec breaks it

This makes it so this RPM works fine on fedora 43, the greeter user
should be created and configured by systemd sysusers anyways

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>

* fix(dms-greeter): use systemd-tmpfiles to set up greeter directories

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>

* fix(rpm, dms-greeter): require systemd for tmpfiles macro

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>

---------

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>
2025-10-30 10:54:37 -04:00
bbedward
baf9b5e6f3 dwl: dont show empty tags 2025-10-30 10:50:35 -04:00
bbedward
c88fc20701 vpn: fix persistence
fixes #587
2025-10-30 09:33:50 -04:00
github-actions[bot]
b1078d6c73 i18n: update translations 2025-10-30 13:22:02 +00:00
bbedward
5033d10246 dwl: remove wlr-randr dependency 2025-10-30 09:21:20 -04:00
bbedward
986993a890 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-29 23:53:43 -04:00
bbedward
19b13a1e81 dwl: tag changes 2025-10-29 23:45:40 -04:00
bbedward
76637fab33 dwl: hide empty tags by default 2025-10-29 23:45:40 -04:00
github-actions[bot]
0a79d9a187 i18n: update translations 2025-10-30 03:39:57 +00:00
github-actions[bot]
36b3b3c7ae i18n: update source strings from codebase 2025-10-30 03:39:47 +00:00
bbedward
8caeca0c08 dwl: tag changes 2025-10-29 23:39:12 -04:00
bbedward
1c323f54ee dwl: hide empty tags by default 2025-10-29 23:07:15 -04:00
bbedward
7ed0b752a8 hyprland: some targeted improvements 2025-10-29 17:23:38 -04:00
bbedward
0569906f7c network: strip down legacy network service 2025-10-29 17:07:19 -04:00
bbedward
2a7cf187ad keyboard layout: remove polling on hyprland 2025-10-29 16:58:07 -04:00
github-actions[bot]
cc5b98a5d2 i18n: update source strings from codebase 2025-10-29 19:42:59 +00:00
bbedward
1478c92f49 matugen: fix wallpaperengine color generation 2025-10-29 15:41:10 -04:00
github-actions[bot]
e1785a1738 i18n: update translations 2025-10-29 19:10:16 +00:00
github-actions[bot]
44ebd2918c i18n: update source strings from codebase 2025-10-29 19:10:05 +00:00
bbedward
c87fa0de5e sway: add support for sway 2025-10-29 15:08:11 -04:00
bbedward
7b26692c8e dwl: support display scales 2025-10-29 13:45:22 -04:00
bbedward
b294e391e7 settings: don't overflow screen dimensions 2025-10-29 13:34:11 -04:00
bbedward
85f8e362e6 pam: try to avoid racey unlock states 2025-10-29 12:59:35 -04:00
github-actions[bot]
d68a6a1056 i18n: update translations 2025-10-29 16:42:00 +00:00
github-actions[bot]
3dae9c0639 i18n: update source strings from codebase 2025-10-29 16:41:52 +00:00
bbedward
aede6b064a dwl: add dwl/MangoWC support
- Requires dms api v12
- Tags/Workspace support
- MangoWC launcher logo
- dpms off/on support
- logout support
2025-10-29 12:39:31 -04:00
bbedward
76b168020c dash: fix IPC positioning 2025-10-29 10:42:39 -04:00
bbedward
5e36b1454a wallpaper: transition blurred wallpaper layer fixes #579 2025-10-29 09:26:03 -04:00
github-actions[bot]
bd35fbac4d i18n: update source strings from codebase 2025-10-29 13:19:15 +00:00
bbedward
e081ec19cc dankbar: cooldown timer on scrolling workspaces 2025-10-29 09:18:46 -04:00
github-actions[bot]
d870d8bad6 i18n: update translations 2025-10-29 13:12:54 +00:00
bbedward
20fd13c836 dankbar: scroll wheels to cycle apps and workspaces 2025-10-29 09:12:10 -04:00
github-actions[bot]
59f98b151d i18n: update source strings from codebase 2025-10-28 20:40:45 +00:00
bbedward
4ac1990c12 systray: fix icon fallback 2025-10-28 16:40:13 -04:00
bbedward
0a5105cc62 niri: simple blur rule 2025-10-28 12:49:49 -04:00
bbedward
a9f8b835ee notepad: use a mask over content area 2025-10-28 12:08:18 -04:00
bbedward
0109bd5bda Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-28 11:37:50 -04:00
bbedward
01dad64c6d notepad: fix mousearea width
fixes #569
2025-10-28 11:37:24 -04:00
bbedward
ee38f57f6d filebrowser: use NF icons 2025-10-28 11:36:18 -04:00
github-actions[bot]
6b163dcb5f Update VERSION to v0.2.5 (from DMS) 2025-10-28 15:29:32 +00:00
github-actions[bot]
baadbbc65a i18n: update source strings from codebase 2025-10-28 15:21:25 +00:00
bbedward
13a2813db9 calendar: parse event links from description 2025-10-28 11:20:42 -04:00
bbedward
cfa7d12dd3 matugen: auto re-load kitty config on color changes 2025-10-28 11:01:26 -04:00
bbedward
8bf23d820f sounds: fix search path to include generic data location
fixes #566
2025-10-28 10:42:01 -04:00
github-actions[bot]
3c7e903ace i18n: update source strings from codebase 2025-10-28 14:20:34 +00:00
bbedward
ee0e3aece9 color: hide picker instantly when spawning hyprpicker
fixes #575
2025-10-28 10:19:56 -04:00
bbedward
d7efd1b285 wallpaper: fix auto cycling initialization 2025-10-28 09:42:50 -04:00
github-actions[bot]
34f7a7ab18 i18n: update translations 2025-10-28 13:32:30 +00:00
bbedward
695eb0a401 i18n: add chinese traiditonal 2025-10-28 09:31:45 -04:00
github-actions[bot]
0d44b95a40 i18n: update translations 2025-10-28 12:55:57 +00:00
bbedward
116c421492 net: don't force singleActive on VPNs
fixes #574
2025-10-28 08:54:56 -04:00
github-actions[bot]
53507ef56b i18n: update translations 2025-10-28 00:21:15 +00:00
bbedward
3c049e031f niri: add workspace assignment helper 2025-10-27 20:20:34 -04:00
bbedward
b6688adb35 niri: more blur on overview 2025-10-27 17:54:53 -04:00
bbedward
b46fe28c05 niri: generate wpblur.kdl 2025-10-27 17:21:26 -04:00
bbedward
e7debdcf46 weather: switch to ip-api for auto location 2025-10-27 16:14:07 -04:00
bbedward
2c2930e876 proc: timeout to CLI helper 2025-10-27 16:13:10 -04:00
github-actions[bot]
ca294fc049 i18n: update translations 2025-10-27 19:38:34 +00:00
github-actions[bot]
86d1a40299 i18n: update source strings from codebase 2025-10-27 19:38:27 +00:00
bbedward
7a3884a633 wallpaper: fix per-monitor dankdash tab + allow matugen override
per-monitor

fixes #561
2025-10-27 15:36:07 -04:00
bbedward
7e5c6581c9 dsearch: rewrite to use CLI instead of api 2025-10-27 15:13:29 -04:00
github-actions[bot]
f17bbbd689 i18n: update source strings from codebase 2025-10-27 18:36:38 +00:00
bbedward
24b046e9d7 dankbar: fix app icon scaling fixes #568 2025-10-27 14:36:15 -04:00
github-actions[bot]
48a7d24c11 i18n: update source strings from codebase 2025-10-27 18:18:05 +00:00
bbedward
033f96a4b0 vpn: various state management fixes 2025-10-27 14:17:29 -04:00
bbedward
f0a1cb6525 icon: fix nerd font path 2025-10-27 11:44:17 -04:00
bbedward
db5782783b files: fix more icon map 2025-10-27 11:35:18 -04:00
bbedward
29022e260d file search: fix more icon mappings 2025-10-27 11:32:07 -04:00
bbedward
1e1f58d3ed fix archive map 2025-10-27 11:29:36 -04:00
github-actions[bot]
12389e2856 i18n: update source strings from codebase 2025-10-27 15:27:51 +00:00
bbedward
cde7427449 file search: add a bunch of nerd icon mappings 2025-10-27 11:27:15 -04:00
github-actions[bot]
42e7cb7b5f i18n: update translations 2025-10-27 15:00:04 +00:00
github-actions[bot]
d7992bc1f7 i18n: update source strings from codebase 2025-10-27 14:59:58 +00:00
bbedward
61c8549401 audio: strip file:// prefix for local dir sounds 2025-10-27 10:59:07 -04:00
bbedward
a284dcf61d i18n: add italian 2025-10-27 10:57:48 -04:00
bbedward
2e462b0899 spotlight: fix file search keyboard navi woes 2025-10-27 10:57:12 -04:00
bbedward
b79c66d59a plugins: fix listview scroll issues
fixes #564
2025-10-27 10:53:29 -04:00
Bruno Cesar Rocha
2f2020e7e2 fix: Load launcher plugin no-trigger settings. (#567)
getPluginTrigger() function only loaded the trigger setting but completely ignored the
noTrigger boolean setting.

When noTrigger was enabled and saved as:
- noTrigger: true
- trigger: ""

On reboot, the function would load trigger which was "", but since empty string is falsy
in the fallback expression, it would revert to plugin.trigger || "!" from the
plugin.json manifest, which is "=" for the Calculator plugin.
2025-10-27 09:55:15 -04:00
github-actions[bot]
b7e99c0d2b i18n: update translations 2025-10-27 13:50:04 +00:00
github-actions[bot]
2648848898 i18n: update source strings from codebase 2025-10-27 13:49:59 +00:00
bbedward
79b23ca829 spotlight: danksearch integration (indexed file search) 2025-10-27 09:49:17 -04:00
bbedward
0ac5b7bc87 workspaces: add desktop entries binding 2025-10-26 22:35:45 -04:00
bbedward
1d211e8474 dock: remove un-used mousearea 2025-10-26 22:32:33 -04:00
github-actions[bot]
1981a83e82 i18n: update translations 2025-10-26 14:19:04 +00:00
Massimo Branchini
cac071e7af filebrowser: use xdg paths (#555) 2025-10-26 10:18:28 -04:00
github-actions[bot]
c6efccd61c i18n: update source strings from codebase 2025-10-26 13:07:38 +00:00
ahoyiski
a90b00e5fe Added a Compact mode to the keyboard_layout_name widget. (#554)
* Added a Compact mode to the keyboard_layout_name widget.

* Added another root.currentLayout = "Unknown" that was missing.
2025-10-26 09:07:11 -04:00
github-actions[bot]
7863d03282 i18n: update source strings from codebase 2025-10-26 03:31:46 +00:00
bbedward
968606d781 filebrowser: improved file browser 2025-10-25 23:30:33 -04:00
bbedward
f7e8de2556 fix section 2025-10-25 18:39:29 -04:00
bbedward
17a8edc1ae greetd: disable hypr logo 2025-10-25 18:39:06 -04:00
github-actions[bot]
30dc63c801 Update VERSION to v0.2.4 (from DMS) 2025-10-25 22:33:32 +00:00
bbedward
8db7b8419a remove font check 2025-10-25 18:30:24 -04:00
bbedward
8c626b20e1 dankbar: fix center section spacers 2025-10-25 18:21:22 -04:00
github-actions[bot]
a8929c8046 i18n: update translations 2025-10-25 21:58:03 +00:00
bbedward
f8e4b5e958 remove font deps from copr 2025-10-25 17:57:20 -04:00
bbedward
58cae24157 fonts: bundle Inter + FiraCode Nerd
- remove all font dependencies
2025-10-25 17:53:08 -04:00
bbedward
bb4f5f37cc Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-25 16:16:16 -04:00
bbedward
237333941a font: bundle in material symbols rounded 2025-10-25 16:15:15 -04:00
131 changed files with 15421 additions and 2805 deletions

View File

@@ -24,6 +24,8 @@ assignees: ""
- [ ] niri - [ ] niri
- [ ] Hyprland - [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
- [ ] Other (specify) - [ ] Other (specify)
## Distribution ## Distribution

View File

@@ -21,6 +21,8 @@ Is this feature specific to one compositor?
- [ ] All compositors - [ ] All compositors
- [ ] niri - [ ] niri
- [ ] Hyprland - [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
## Proposed Solution ## Proposed Solution

View File

@@ -10,6 +10,8 @@ assignees: ""
- [ ] niri - [ ] niri
- [ ] Hyprland - [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
- [ ] other - [ ] other
## Distribution ## Distribution

View File

@@ -91,9 +91,6 @@ jobs:
Requires: accountsservice Requires: accountsservice
Requires: dms-cli Requires: dms-cli
Requires: dgop Requires: dgop
Requires: fira-code-fonts
Requires: material-symbols-fonts
Requires: rsms-inter-fonts
Recommends: brightnessctl Recommends: brightnessctl
Recommends: cava Recommends: cava

View File

@@ -112,8 +112,10 @@ jobs:
LANGUAGES=( LANGUAGES=(
"ja:translations/poexports/ja.json" "ja:translations/poexports/ja.json"
"zh-Hans:translations/poexports/zh_CN.json" "zh-Hans:translations/poexports/zh_CN.json"
"zh-Hant:translations/poexports/zh_TW.json"
"pt-br:translations/poexports/pt.json" "pt-br:translations/poexports/pt.json"
"tr:translations/poexports/tr.json" "tr:translations/poexports/tr.json"
"it:translations/poexports/it.json"
) )
ANY_CHANGED=false ANY_CHANGED=false

View File

@@ -49,9 +49,9 @@ jobs:
set -e set -e
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "") PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" --author='^(?!github-actions\[bot\])' | head -50) CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
else else
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" --author='^(?!github-actions\[bot\])' "${PREVIOUS_TAG}..${TAG}") CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
fi fi
cat > RELEASE_BODY.md << 'EOF' cat > RELEASE_BODY.md << 'EOF'

View File

@@ -22,6 +22,57 @@ Singleton {
property string wallpaperLastPath: "" property string wallpaperLastPath: ""
property string profileLastPath: "" property string profileLastPath: ""
property var fileBrowserSettings: ({
"wallpaper": {
"lastPath": "",
"viewMode": "grid",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"profile": {
"lastPath": "",
"viewMode": "grid",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"notepad_save": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"notepad_load": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"generic": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"default": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
}
})
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
loadCache() loadCache()
@@ -43,6 +94,37 @@ Singleton {
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "" wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : "" profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""
if (cache.fileBrowserSettings !== undefined) {
fileBrowserSettings = cache.fileBrowserSettings
} else if (cache.fileBrowserViewMode !== undefined) {
fileBrowserSettings = {
"wallpaper": {
"lastPath": cache.wallpaperLastPath || "",
"viewMode": cache.fileBrowserViewMode || "grid",
"sortBy": cache.fileBrowserSortBy || "name",
"sortAscending": cache.fileBrowserSortAscending !== undefined ? cache.fileBrowserSortAscending : true,
"iconSizeIndex": cache.fileBrowserIconSizeIndex !== undefined ? cache.fileBrowserIconSizeIndex : 1,
"showSidebar": cache.fileBrowserShowSidebar !== undefined ? cache.fileBrowserShowSidebar : true
},
"profile": {
"lastPath": cache.profileLastPath || "",
"viewMode": cache.fileBrowserViewMode || "grid",
"sortBy": cache.fileBrowserSortBy || "name",
"sortAscending": cache.fileBrowserSortAscending !== undefined ? cache.fileBrowserSortAscending : true,
"iconSizeIndex": cache.fileBrowserIconSizeIndex !== undefined ? cache.fileBrowserIconSizeIndex : 1,
"showSidebar": cache.fileBrowserShowSidebar !== undefined ? cache.fileBrowserShowSidebar : true
},
"file": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
}
}
}
if (cache.configVersion === undefined) { if (cache.configVersion === undefined) {
migrateFromUndefinedToV1(cache) migrateFromUndefinedToV1(cache)
cleanupUnusedKeys() cleanupUnusedKeys()
@@ -62,6 +144,7 @@ Singleton {
cacheFile.setText(JSON.stringify({ cacheFile.setText(JSON.stringify({
"wallpaperLastPath": wallpaperLastPath, "wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath, "profileLastPath": profileLastPath,
"fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion "configVersion": cacheConfigVersion
}, null, 2)) }, null, 2))
} }
@@ -74,6 +157,7 @@ Singleton {
const validKeys = [ const validKeys = [
"wallpaperLastPath", "wallpaperLastPath",
"profileLastPath", "profileLastPath",
"fileBrowserSettings",
"configVersion" "configVersion"
] ]

View File

@@ -9,20 +9,24 @@ Singleton {
id: root id: root
property int defaultDebounceMs: 50 property int defaultDebounceMs: 50
property var _procDebouncers: ({}) // id -> { timer, command, callback, waitMs } property int defaultTimeoutMs: 10000
property var _procDebouncers: ({})
function runCommand(id, command, callback, debounceMs) { function runCommand(id, command, callback, debounceMs, timeoutMs) {
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
const timeout = (typeof timeoutMs === "number" && timeoutMs > 0) ? timeoutMs : defaultTimeoutMs
let procId = id ? id : Math.random() let procId = id ? id : Math.random()
const isRandomId = !id
if (!_procDebouncers[procId]) { if (!_procDebouncers[procId]) {
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root) const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
t.triggered.connect(function() { _launchProc(procId) }) t.triggered.connect(function() { _launchProc(procId, isRandomId) })
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait } _procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait, timeoutMs: timeout, isRandomId: isRandomId }
} else { } else {
_procDebouncers[procId].command = command _procDebouncers[procId].command = command
_procDebouncers[procId].callback = callback _procDebouncers[procId].callback = callback
_procDebouncers[procId].waitMs = wait _procDebouncers[procId].waitMs = wait
_procDebouncers[procId].timeoutMs = timeout
} }
const entry = _procDebouncers[procId] const entry = _procDebouncers[procId]
@@ -30,41 +34,77 @@ Singleton {
entry.timer.restart() entry.timer.restart()
} }
function _launchProc(id) { function _launchProc(id, isRandomId) {
const entry = _procDebouncers[id] const entry = _procDebouncers[id]
if (!entry) return if (!entry) return
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root) const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root)
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc) const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc) const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
proc.stdout = out proc.stdout = out
proc.stderr = err proc.stderr = err
proc.command = entry.command proc.command = entry.command
let capturedOut = "" let capturedOut = ""
let capturedErr = ""
let exitSeen = false let exitSeen = false
let exitCodeValue = -1 let exitCodeValue = -1
let outSeen = false
let errSeen = false
let timedOut = false
timeoutTimer.interval = entry.timeoutMs
timeoutTimer.triggered.connect(function() {
if (!exitSeen) {
timedOut = true
proc.running = false
exitSeen = true
exitCodeValue = 124
maybeComplete()
}
})
out.streamFinished.connect(function() { out.streamFinished.connect(function() {
capturedOut = out.text || "" capturedOut = out.text || ""
outSeen = true
maybeComplete()
})
err.streamFinished.connect(function() {
capturedErr = err.text || ""
errSeen = true
maybeComplete() maybeComplete()
}) })
proc.exited.connect(function(code) { proc.exited.connect(function(code) {
timeoutTimer.stop()
exitSeen = true exitSeen = true
exitCodeValue = code exitCodeValue = code
maybeComplete() maybeComplete()
}) })
function maybeComplete() { function maybeComplete() {
if (!exitSeen) return if (!exitSeen || !outSeen || !errSeen) return
timeoutTimer.stop()
if (typeof entry.callback === "function") { if (typeof entry.callback === "function") {
try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) } try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) }
} }
try { proc.destroy() } catch (_) {} try { proc.destroy() } catch (_) {}
try { timeoutTimer.destroy() } catch (_) {}
if (isRandomId || entry.isRandomId) {
Qt.callLater(function() {
if (_procDebouncers[id]) {
try { _procDebouncers[id].timer.destroy() } catch (_) {}
delete _procDebouncers[id]
}
})
}
} }
proc.running = true proc.running = true
timeoutTimer.start()
} }
} }

View File

@@ -1,4 +1,5 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore import QtCore
@@ -144,6 +145,10 @@ Singleton {
Theme.generateSystemThemesFromCurrentTheme() Theme.generateSystemThemesFromCurrentTheme()
} }
} }
if (typeof WallpaperCyclingService !== "undefined") {
WallpaperCyclingService.updateCyclingState()
}
} }
} catch (e) { } catch (e) {
@@ -151,7 +156,8 @@ Singleton {
} }
function saveSettings() { function saveSettings() {
if (isGreeterMode) return if (isGreeterMode)
return
settingsFile.setText(JSON.stringify({ settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode, "isLightMode": isLightMode,
"wallpaperPath": wallpaperPath, "wallpaperPath": wallpaperPath,
@@ -244,23 +250,12 @@ Singleton {
} }
function cleanupUnusedKeys() { function cleanupUnusedKeys() {
const validKeys = [ const validKeys = ["isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper", "wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight", "monitorWallpapersDark", "doNotDisturb", "nightModeEnabled", "nightModeTemperature", "nightModeAutoEnabled", "nightModeAutoMode", "nightModeStartHour", "nightModeStartMinute", "nightModeEndHour", "nightModeEndMinute", "latitude", "longitude", "nightModeUseIPLocation", "nightModeLocationProvider", "pinnedApps", "selectedGpuIndex", "nvidiaGpuTempEnabled", "nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled", "wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime", "monitorCyclingSettings", "lastBrightnessDevice", "launchPrefix", "wallpaperTransition", "includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"]
"isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper",
"wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight",
"monitorWallpapersDark", "doNotDisturb", "nightModeEnabled",
"nightModeTemperature", "nightModeAutoEnabled", "nightModeAutoMode",
"nightModeStartHour", "nightModeStartMinute", "nightModeEndHour",
"nightModeEndMinute", "latitude", "longitude", "nightModeUseIPLocation", "nightModeLocationProvider",
"pinnedApps", "selectedGpuIndex", "nvidiaGpuTempEnabled",
"nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled",
"wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime",
"monitorCyclingSettings", "lastBrightnessDevice", "launchPrefix", "wallpaperTransition",
"includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"
]
try { try {
const content = settingsFile.text() const content = settingsFile.text()
if (!content || !content.trim()) return if (!content || !content.trim())
return
const settings = JSON.parse(content) const settings = JSON.parse(content)
let needsSave = false let needsSave = false
@@ -425,13 +420,16 @@ Singleton {
saveSettings() saveSettings()
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined") { if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined" && typeof SettingsData !== "undefined") {
var screens = Quickshell.screens var screens = Quickshell.screens
if (screens.length > 0 && screenName === screens[0].name) { if (screens.length > 0) {
var targetMonitor = (SettingsData.matugenTargetMonitor && SettingsData.matugenTargetMonitor !== "") ? SettingsData.matugenTargetMonitor : screens[0].name
if (screenName === targetMonitor) {
Theme.generateSystemThemesFromCurrentTheme() Theme.generateSystemThemesFromCurrentTheme()
} }
} }
} }
}
function setWallpaperTransition(transition) { function setWallpaperTransition(transition) {
wallpaperTransition = transition wallpaperTransition = transition
@@ -461,7 +459,12 @@ Singleton {
function setMonitorCyclingEnabled(screenName, enabled) { function setMonitorCyclingEnabled(screenName, enabled) {
var newSettings = Object.assign({}, monitorCyclingSettings) var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) { if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" } newSettings[screenName] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
} }
newSettings[screenName].enabled = enabled newSettings[screenName].enabled = enabled
monitorCyclingSettings = newSettings monitorCyclingSettings = newSettings
@@ -471,7 +474,12 @@ Singleton {
function setMonitorCyclingMode(screenName, mode) { function setMonitorCyclingMode(screenName, mode) {
var newSettings = Object.assign({}, monitorCyclingSettings) var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) { if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" } newSettings[screenName] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
} }
newSettings[screenName].mode = mode newSettings[screenName].mode = mode
monitorCyclingSettings = newSettings monitorCyclingSettings = newSettings
@@ -481,7 +489,12 @@ Singleton {
function setMonitorCyclingInterval(screenName, interval) { function setMonitorCyclingInterval(screenName, interval) {
var newSettings = Object.assign({}, monitorCyclingSettings) var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) { if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" } newSettings[screenName] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
} }
newSettings[screenName].interval = interval newSettings[screenName].interval = interval
monitorCyclingSettings = newSettings monitorCyclingSettings = newSettings
@@ -491,7 +504,12 @@ Singleton {
function setMonitorCyclingTime(screenName, time) { function setMonitorCyclingTime(screenName, time) {
var newSettings = Object.assign({}, monitorCyclingSettings) var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) { if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" } newSettings[screenName] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
} }
newSettings[screenName].time = time newSettings[screenName].time = time
monitorCyclingSettings = newSettings monitorCyclingSettings = newSettings
@@ -592,7 +610,8 @@ Singleton {
let recent = recentColors.slice() let recent = recentColors.slice()
recent = recent.filter(c => c !== colorStr) recent = recent.filter(c => c !== colorStr)
recent.unshift(colorStr) recent.unshift(colorStr)
if (recent.length > 5) recent = recent.slice(0, 5) if (recent.length > 5)
recent = recent.slice(0, 5)
recentColors = recent recentColors = recent
saveSettings() saveSettings()
} }
@@ -633,7 +652,8 @@ Singleton {
} }
function syncWallpaperForCurrentMode() { function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) return if (!perModeWallpaper)
return
if (perMonitorWallpaper) { if (perMonitorWallpaper) {
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark) monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
@@ -652,10 +672,10 @@ Singleton {
function getMonitorCyclingSettings(screenName) { function getMonitorCyclingSettings(screenName) {
return monitorCyclingSettings[screenName] || { return monitorCyclingSettings[screenName] || {
enabled: false, "enabled": false,
mode: "interval", "mode": "interval",
interval: 300, "interval": 300,
time: "06:00" "time": "06:00"
} }
} }

View File

@@ -51,6 +51,7 @@ Singleton {
property string customThemeFile: "" property string customThemeFile: ""
property string matugenScheme: "scheme-tonal-spot" property string matugenScheme: "scheme-tonal-spot"
property bool runUserMatugenTemplates: true property bool runUserMatugenTemplates: true
property string matugenTargetMonitor: ""
property real dankBarTransparency: 1.0 property real dankBarTransparency: 1.0
property real dankBarWidgetTransparency: 1.0 property real dankBarWidgetTransparency: 1.0
property real popupTransparency: 1.0 property real popupTransparency: 1.0
@@ -90,16 +91,39 @@ Singleton {
property bool controlCenterShowNetworkIcon: true property bool controlCenterShowNetworkIcon: true
property bool controlCenterShowBluetoothIcon: true property bool controlCenterShowBluetoothIcon: true
property bool controlCenterShowAudioIcon: true property bool controlCenterShowAudioIcon: true
property var controlCenterWidgets: [ property var controlCenterWidgets: [{
{"id": "volumeSlider", "enabled": true, "width": 50}, "id": "volumeSlider",
{"id": "brightnessSlider", "enabled": true, "width": 50}, "enabled": true,
{"id": "wifi", "enabled": true, "width": 50}, "width": 50
{"id": "bluetooth", "enabled": true, "width": 50}, }, {
{"id": "audioOutput", "enabled": true, "width": 50}, "id": "brightnessSlider",
{"id": "audioInput", "enabled": true, "width": 50}, "enabled": true,
{"id": "nightMode", "enabled": true, "width": 50}, "width": 50
{"id": "darkMode", "enabled": true, "width": 50} }, {
] "id": "wifi",
"enabled": true,
"width": 50
}, {
"id": "bluetooth",
"enabled": true,
"width": 50
}, {
"id": "audioOutput",
"enabled": true,
"width": 50
}, {
"id": "audioInput",
"enabled": true,
"width": 50
}, {
"id": "nightMode",
"enabled": true,
"width": 50
}, {
"id": "darkMode",
"enabled": true,
"width": 50
}]
property bool showWorkspaceIndex: false property bool showWorkspaceIndex: false
property bool showWorkspacePadding: false property bool showWorkspacePadding: false
@@ -107,11 +131,13 @@ Singleton {
property bool showWorkspaceApps: false property bool showWorkspaceApps: false
property int maxWorkspaceIcons: 3 property int maxWorkspaceIcons: 3
property bool workspacesPerMonitor: true property bool workspacesPerMonitor: true
property bool dwlShowAllTags: false
property var workspaceNameIcons: ({}) property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true property bool waveProgressEnabled: true
property bool clockCompactMode: false property bool clockCompactMode: false
property bool focusedWindowCompactMode: false property bool focusedWindowCompactMode: false
property bool runningAppsCompactMode: true property bool runningAppsCompactMode: true
property bool keyboardLayoutNameCompactMode: false
property bool runningAppsCurrentWorkspace: false property bool runningAppsCurrentWorkspace: false
property bool runningAppsGroupByApp: false property bool runningAppsGroupByApp: false
property string clockDateFormat: "" property string clockDateFormat: ""
@@ -133,6 +159,7 @@ Singleton {
property bool weatherEnabled: true property bool weatherEnabled: true
property string networkPreference: "auto" property string networkPreference: "auto"
property string vpnLastConnected: ""
property string iconTheme: "System Default" property string iconTheme: "System Default"
property var availableIconThemes: ["System Default"] property var availableIconThemes: ["System Default"]
@@ -271,7 +298,6 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
loadSettings() loadSettings()
fontCheckTimer.start()
initializeListModels() initializeListModels()
fprintdDetectionProcess.running = true fprintdDetectionProcess.running = true
} }
@@ -330,8 +356,14 @@ Singleton {
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "" customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot" matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"
runUserMatugenTemplates = settings.runUserMatugenTemplates !== undefined ? settings.runUserMatugenTemplates : true runUserMatugenTemplates = settings.runUserMatugenTemplates !== undefined ? settings.runUserMatugenTemplates : true
dankBarTransparency = settings.dankBarTransparency !== undefined ? (settings.dankBarTransparency > 1 ? settings.dankBarTransparency / 100 : settings.dankBarTransparency) : (settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 1.0) matugenTargetMonitor = settings.matugenTargetMonitor !== undefined ? settings.matugenTargetMonitor : ""
dankBarWidgetTransparency = settings.dankBarWidgetTransparency !== undefined ? (settings.dankBarWidgetTransparency > 1 ? settings.dankBarWidgetTransparency / 100 : settings.dankBarWidgetTransparency) : (settings.topBarWidgetTransparency !== undefined ? (settings.topBarWidgetTransparency > 1 ? settings.topBarWidgetTransparency / 100 : settings.topBarWidgetTransparency) : 1.0) dankBarTransparency = settings.dankBarTransparency
!== undefined ? (settings.dankBarTransparency > 1 ? settings.dankBarTransparency
/ 100 : settings.dankBarTransparency) : (settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 1.0)
dankBarWidgetTransparency = settings.dankBarWidgetTransparency
!== undefined ? (settings.dankBarWidgetTransparency
> 1 ? settings.dankBarWidgetTransparency
/ 100 : settings.dankBarWidgetTransparency) : (settings.topBarWidgetTransparency !== undefined ? (settings.topBarWidgetTransparency > 1 ? settings.topBarWidgetTransparency / 100 : settings.topBarWidgetTransparency) : 1.0)
popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 1.0 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 dockTransparency = settings.dockTransparency !== undefined ? (settings.dockTransparency > 1 ? settings.dockTransparency / 100 : settings.dockTransparency) : 1
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true
@@ -362,16 +394,39 @@ Singleton {
controlCenterShowNetworkIcon = settings.controlCenterShowNetworkIcon !== undefined ? settings.controlCenterShowNetworkIcon : true controlCenterShowNetworkIcon = settings.controlCenterShowNetworkIcon !== undefined ? settings.controlCenterShowNetworkIcon : true
controlCenterShowBluetoothIcon = settings.controlCenterShowBluetoothIcon !== undefined ? settings.controlCenterShowBluetoothIcon : true controlCenterShowBluetoothIcon = settings.controlCenterShowBluetoothIcon !== undefined ? settings.controlCenterShowBluetoothIcon : true
controlCenterShowAudioIcon = settings.controlCenterShowAudioIcon !== undefined ? settings.controlCenterShowAudioIcon : true controlCenterShowAudioIcon = settings.controlCenterShowAudioIcon !== undefined ? settings.controlCenterShowAudioIcon : true
controlCenterWidgets = settings.controlCenterWidgets !== undefined ? settings.controlCenterWidgets : [ controlCenterWidgets = settings.controlCenterWidgets !== undefined ? settings.controlCenterWidgets : [{
{"id": "volumeSlider", "enabled": true, "width": 50}, "id": "volumeSlider",
{"id": "brightnessSlider", "enabled": true, "width": 50}, "enabled": true,
{"id": "wifi", "enabled": true, "width": 50}, "width": 50
{"id": "bluetooth", "enabled": true, "width": 50}, }, {
{"id": "audioOutput", "enabled": true, "width": 50}, "id": "brightnessSlider",
{"id": "audioInput", "enabled": true, "width": 50}, "enabled": true,
{"id": "nightMode", "enabled": true, "width": 50}, "width": 50
{"id": "darkMode", "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 showWorkspaceIndex = settings.showWorkspaceIndex !== undefined ? settings.showWorkspaceIndex : false
showWorkspacePadding = settings.showWorkspacePadding !== undefined ? settings.showWorkspacePadding : false showWorkspacePadding = settings.showWorkspacePadding !== undefined ? settings.showWorkspacePadding : false
workspaceScrolling = settings.workspaceScrolling !== undefined ? settings.workspaceScrolling : false workspaceScrolling = settings.workspaceScrolling !== undefined ? settings.workspaceScrolling : false
@@ -379,10 +434,12 @@ Singleton {
maxWorkspaceIcons = settings.maxWorkspaceIcons !== undefined ? settings.maxWorkspaceIcons : 3 maxWorkspaceIcons = settings.maxWorkspaceIcons !== undefined ? settings.maxWorkspaceIcons : 3
workspaceNameIcons = settings.workspaceNameIcons !== undefined ? settings.workspaceNameIcons : ({}) workspaceNameIcons = settings.workspaceNameIcons !== undefined ? settings.workspaceNameIcons : ({})
workspacesPerMonitor = settings.workspacesPerMonitor !== undefined ? settings.workspacesPerMonitor : true workspacesPerMonitor = settings.workspacesPerMonitor !== undefined ? settings.workspacesPerMonitor : true
dwlShowAllTags = settings.dwlShowAllTags !== undefined ? settings.dwlShowAllTags : false
waveProgressEnabled = settings.waveProgressEnabled !== undefined ? settings.waveProgressEnabled : true waveProgressEnabled = settings.waveProgressEnabled !== undefined ? settings.waveProgressEnabled : true
clockCompactMode = settings.clockCompactMode !== undefined ? settings.clockCompactMode : false clockCompactMode = settings.clockCompactMode !== undefined ? settings.clockCompactMode : false
focusedWindowCompactMode = settings.focusedWindowCompactMode !== undefined ? settings.focusedWindowCompactMode : false focusedWindowCompactMode = settings.focusedWindowCompactMode !== undefined ? settings.focusedWindowCompactMode : false
runningAppsCompactMode = settings.runningAppsCompactMode !== undefined ? settings.runningAppsCompactMode : true runningAppsCompactMode = settings.runningAppsCompactMode !== undefined ? settings.runningAppsCompactMode : true
keyboardLayoutNameCompactMode = settings.keyboardLayoutNameCompactMode !== undefined ? settings.keyboardLayoutNameCompactMode : true
runningAppsCurrentWorkspace = settings.runningAppsCurrentWorkspace !== undefined ? settings.runningAppsCurrentWorkspace : false runningAppsCurrentWorkspace = settings.runningAppsCurrentWorkspace !== undefined ? settings.runningAppsCurrentWorkspace : false
runningAppsGroupByApp = settings.runningAppsGroupByApp !== undefined ? settings.runningAppsGroupByApp : false runningAppsGroupByApp = settings.runningAppsGroupByApp !== undefined ? settings.runningAppsGroupByApp : false
clockDateFormat = settings.clockDateFormat !== undefined ? settings.clockDateFormat : "" clockDateFormat = settings.clockDateFormat !== undefined ? settings.clockDateFormat : ""
@@ -402,7 +459,8 @@ Singleton {
} else { } else {
var leftWidgets = settings.dankBarLeftWidgets !== undefined ? settings.dankBarLeftWidgets : (settings.topBarLeftWidgets !== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"]) var leftWidgets = settings.dankBarLeftWidgets !== undefined ? settings.dankBarLeftWidgets : (settings.topBarLeftWidgets !== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"])
var centerWidgets = settings.dankBarCenterWidgets !== undefined ? settings.dankBarCenterWidgets : (settings.topBarCenterWidgets !== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"]) var centerWidgets = settings.dankBarCenterWidgets !== undefined ? settings.dankBarCenterWidgets : (settings.topBarCenterWidgets !== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"])
var rightWidgets = settings.dankBarRightWidgets !== undefined ? settings.dankBarRightWidgets : (settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]) var rightWidgets = settings.dankBarRightWidgets
!== undefined ? settings.dankBarRightWidgets : (settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"])
dankBarLeftWidgets = leftWidgets dankBarLeftWidgets = leftWidgets
dankBarCenterWidgets = centerWidgets dankBarCenterWidgets = centerWidgets
dankBarRightWidgets = rightWidgets dankBarRightWidgets = rightWidgets
@@ -414,6 +472,7 @@ Singleton {
spotlightModalViewMode = settings.spotlightModalViewMode !== undefined ? settings.spotlightModalViewMode : "list" spotlightModalViewMode = settings.spotlightModalViewMode !== undefined ? settings.spotlightModalViewMode : "list"
sortAppsAlphabetically = settings.sortAppsAlphabetically !== undefined ? settings.sortAppsAlphabetically : false sortAppsAlphabetically = settings.sortAppsAlphabetically !== undefined ? settings.sortAppsAlphabetically : false
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto" networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto"
vpnLastConnected = settings.vpnLastConnected !== undefined ? settings.vpnLastConnected : ""
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default" iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default"
if (settings.useOSLogo !== undefined) { if (settings.useOSLogo !== undefined) {
launcherLogoMode = settings.useOSLogo ? "os" : "apps" launcherLogoMode = settings.useOSLogo ? "os" : "apps"
@@ -474,9 +533,9 @@ Singleton {
customPowerActionHibernate = settings.customPowerActionHibernate != undefined ? settings.customPowerActionHibernate : "" customPowerActionHibernate = settings.customPowerActionHibernate != undefined ? settings.customPowerActionHibernate : ""
customPowerActionReboot = settings.customPowerActionReboot != undefined ? settings.customPowerActionReboot : "" customPowerActionReboot = settings.customPowerActionReboot != undefined ? settings.customPowerActionReboot : ""
customPowerActionPowerOff = settings.customPowerActionPowerOff != undefined ? settings.customPowerActionPowerOff : "" customPowerActionPowerOff = settings.customPowerActionPowerOff != undefined ? settings.customPowerActionPowerOff : ""
updaterUseCustomCommand = settings.updaterUseCustomCommand !== undefined ? settings.updaterUseCustomCommand : false; updaterUseCustomCommand = settings.updaterUseCustomCommand !== undefined ? settings.updaterUseCustomCommand : false
updaterCustomCommand = settings.updaterCustomCommand !== undefined ? settings.updaterCustomCommand : ""; updaterCustomCommand = settings.updaterCustomCommand !== undefined ? settings.updaterCustomCommand : ""
updaterTerminalAdditionalParams = settings.updaterTerminalAdditionalParams !== undefined ? settings.updaterTerminalAdditionalParams : ""; updaterTerminalAdditionalParams = settings.updaterTerminalAdditionalParams !== undefined ? settings.updaterTerminalAdditionalParams : ""
dankBarSpacing = settings.dankBarSpacing !== undefined ? settings.dankBarSpacing : (settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4) dankBarSpacing = settings.dankBarSpacing !== undefined ? settings.dankBarSpacing : (settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4)
dankBarBottomGap = settings.dankBarBottomGap !== undefined ? settings.dankBarBottomGap : (settings.topBarBottomGap !== undefined ? settings.topBarBottomGap : 0) dankBarBottomGap = settings.dankBarBottomGap !== undefined ? settings.dankBarBottomGap : (settings.topBarBottomGap !== undefined ? settings.topBarBottomGap : 0)
dankBarInnerPadding = settings.dankBarInnerPadding !== undefined ? settings.dankBarInnerPadding : (settings.topBarInnerPadding !== undefined ? settings.topBarInnerPadding : 4) dankBarInnerPadding = settings.dankBarInnerPadding !== undefined ? settings.dankBarInnerPadding : (settings.topBarInnerPadding !== undefined ? settings.topBarInnerPadding : 4)
@@ -489,7 +548,8 @@ Singleton {
dankBarBorderThickness = settings.dankBarBorderThickness !== undefined ? settings.dankBarBorderThickness : 1 dankBarBorderThickness = settings.dankBarBorderThickness !== undefined ? settings.dankBarBorderThickness : 1
popupGapsAuto = settings.popupGapsAuto !== undefined ? settings.popupGapsAuto : true popupGapsAuto = settings.popupGapsAuto !== undefined ? settings.popupGapsAuto : true
popupGapsManual = settings.popupGapsManual !== undefined ? settings.popupGapsManual : 4 popupGapsManual = settings.popupGapsManual !== undefined ? settings.popupGapsManual : 4
dankBarPosition = settings.dankBarPosition !== undefined ? settings.dankBarPosition : (settings.dankBarAtBottom !== undefined ? (settings.dankBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : (settings.topBarAtBottom !== undefined ? (settings.topBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : SettingsData.Position.Top)) dankBarPosition = settings.dankBarPosition
!== undefined ? settings.dankBarPosition : (settings.dankBarAtBottom !== undefined ? (settings.dankBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : (settings.topBarAtBottom !== undefined ? (settings.topBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : SettingsData.Position.Top))
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true
enableFprint = settings.enableFprint !== undefined ? settings.enableFprint : false enableFprint = settings.enableFprint !== undefined ? settings.enableFprint : false
maxFprintTries = settings.maxFprintTries !== undefined ? settings.maxFprintTries : 3 maxFprintTries = settings.maxFprintTries !== undefined ? settings.maxFprintTries : 3
@@ -550,6 +610,7 @@ Singleton {
"customThemeFile": customThemeFile, "customThemeFile": customThemeFile,
"matugenScheme": matugenScheme, "matugenScheme": matugenScheme,
"runUserMatugenTemplates": runUserMatugenTemplates, "runUserMatugenTemplates": runUserMatugenTemplates,
"matugenTargetMonitor": matugenTargetMonitor,
"dankBarTransparency": dankBarTransparency, "dankBarTransparency": dankBarTransparency,
"dankBarWidgetTransparency": dankBarWidgetTransparency, "dankBarWidgetTransparency": dankBarWidgetTransparency,
"popupTransparency": popupTransparency, "popupTransparency": popupTransparency,
@@ -589,11 +650,13 @@ Singleton {
"showWorkspaceApps": showWorkspaceApps, "showWorkspaceApps": showWorkspaceApps,
"maxWorkspaceIcons": maxWorkspaceIcons, "maxWorkspaceIcons": maxWorkspaceIcons,
"workspacesPerMonitor": workspacesPerMonitor, "workspacesPerMonitor": workspacesPerMonitor,
"dwlShowAllTags": dwlShowAllTags,
"workspaceNameIcons": workspaceNameIcons, "workspaceNameIcons": workspaceNameIcons,
"waveProgressEnabled": waveProgressEnabled, "waveProgressEnabled": waveProgressEnabled,
"clockCompactMode": clockCompactMode, "clockCompactMode": clockCompactMode,
"focusedWindowCompactMode": focusedWindowCompactMode, "focusedWindowCompactMode": focusedWindowCompactMode,
"runningAppsCompactMode": runningAppsCompactMode, "runningAppsCompactMode": runningAppsCompactMode,
"keyboardLayoutNameCompactMode": keyboardLayoutNameCompactMode,
"runningAppsCurrentWorkspace": runningAppsCurrentWorkspace, "runningAppsCurrentWorkspace": runningAppsCurrentWorkspace,
"runningAppsGroupByApp": runningAppsGroupByApp, "runningAppsGroupByApp": runningAppsGroupByApp,
"clockDateFormat": clockDateFormat, "clockDateFormat": clockDateFormat,
@@ -606,6 +669,7 @@ Singleton {
"spotlightModalViewMode": spotlightModalViewMode, "spotlightModalViewMode": spotlightModalViewMode,
"sortAppsAlphabetically": sortAppsAlphabetically, "sortAppsAlphabetically": sortAppsAlphabetically,
"networkPreference": networkPreference, "networkPreference": networkPreference,
"vpnLastConnected": vpnLastConnected,
"iconTheme": iconTheme, "iconTheme": iconTheme,
"launcherLogoMode": launcherLogoMode, "launcherLogoMode": launcherLogoMode,
"launcherLogoCustomPath": launcherLogoCustomPath, "launcherLogoCustomPath": launcherLogoCustomPath,
@@ -715,52 +779,12 @@ Singleton {
} }
function cleanupUnusedKeys() { function cleanupUnusedKeys() {
const validKeys = [ const validKeys = ["currentThemeName", "customThemeFile", "matugenScheme", "runUserMatugenTemplates", "matugenTargetMonitor", "dankBarTransparency", "dankBarWidgetTransparency", "popupTransparency", "dockTransparency", "use24HourClock", "showSeconds", "useFahrenheit", "nightModeEnabled", "weatherLocation", "weatherCoordinates", "useAutoLocation", "weatherEnabled", "showLauncherButton", "showWorkspaceSwitcher", "showFocusedWindow", "showWeather", "showMusic", "showClipboard", "showCpuUsage", "showMemUsage", "showCpuTemp", "showGpuTemp", "selectedGpuIndex", "enabledGpuPciIds", "showSystemTray", "showClock", "showNotificationButton", "showBattery", "showControlCenterButton", "controlCenterShowNetworkIcon", "controlCenterShowBluetoothIcon", "controlCenterShowAudioIcon", "controlCenterWidgets", "showWorkspaceIndex", "workspaceScrolling", "showWorkspacePadding", "showWorkspaceApps", "maxWorkspaceIcons", "workspacesPerMonitor", "dwlShowAllTags", "workspaceNameIcons", "waveProgressEnabled", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsCurrentWorkspace", "runningAppsGroupByApp", "clockDateFormat", "lockDateFormat", "mediaSize", "dankBarLeftWidgets", "dankBarCenterWidgets", "dankBarRightWidgets", "appLauncherViewMode", "spotlightModalViewMode", "sortAppsAlphabetically", "networkPreference", "vpnLastConnected", "iconTheme", "launcherLogoMode", "launcherLogoCustomPath", "launcherLogoColorOverride", "launcherLogoColorInvertOnMode", "launcherLogoBrightness", "launcherLogoContrast", "launcherLogoSizeOffset", "fontFamily", "monoFontFamily", "fontWeight", "fontScale", "dankBarFontScale", "notepadUseMonospace", "notepadFontFamily", "notepadFontSize", "notepadShowLineNumbers", "notepadTransparencyOverride", "notepadLastCustomTransparency", "soundsEnabled", "useSystemSoundTheme", "soundNewNotification", "soundVolumeChanged", "soundPluggedIn", "gtkThemingEnabled", "qtThemingEnabled", "syncModeWithPortal", "showDock", "dockAutoHide", "dockGroupByApp", "dockOpenOnOverview", "dockPosition", "dockSpacing", "dockBottomGap", "dockIconSize", "dockIndicatorStyle", "cornerRadius", "notificationOverlayEnabled", "dankBarAutoHide", "dankBarOpenOnOverview", "dankBarVisible", "dankBarSpacing", "dankBarBottomGap", "dankBarInnerPadding", "dankBarSquareCorners", "dankBarNoBackground", "dankBarGothCornersEnabled", "dankBarBorderEnabled", "dankBarBorderColor", "dankBarBorderOpacity", "dankBarBorderThickness", "popupGapsAuto", "popupGapsManual", "dankBarPosition", "lockScreenShowPowerActions", "enableFprint", "maxFprintTries", "hideBrightnessSlider", "widgetBackgroundColor", "surfaceBase", "wallpaperFillMode", "blurredWallpaperLayer", "blurWallpaperOnOverview", "notificationTimeoutLow", "notificationTimeoutNormal", "notificationTimeoutCritical", "notificationPopupPosition", "osdAlwaysShowValue", "powerActionConfirm", "customPowerActionLock", "customPowerActionLogout", "customPowerActionSuspend", "customPowerActionHibernate", "customPowerActionReboot", "customPowerActionPowerOff", "updaterUseCustomCommand", "updaterCustomCommand", "updaterTerminalAdditionalParams", "screenPreferences", "showOnLastDisplay", "animationSpeed", "customAnimationDuration", "acMonitorTimeout", "acLockTimeout", "acSuspendTimeout", "acHibernateTimeout", "batteryMonitorTimeout", "batteryLockTimeout", "batterySuspendTimeout", "batteryHibernateTimeout", "lockBeforeSuspend", "loginctlLockIntegration", "launchPrefix", "brightnessDevicePins", "configVersion"]
"currentThemeName", "customThemeFile", "matugenScheme", "runUserMatugenTemplates",
"dankBarTransparency", "dankBarWidgetTransparency", "popupTransparency", "dockTransparency",
"use24HourClock", "showSeconds", "useFahrenheit", "nightModeEnabled", "weatherLocation",
"weatherCoordinates", "useAutoLocation", "weatherEnabled", "showLauncherButton",
"showWorkspaceSwitcher", "showFocusedWindow", "showWeather", "showMusic",
"showClipboard", "showCpuUsage", "showMemUsage", "showCpuTemp", "showGpuTemp",
"selectedGpuIndex", "enabledGpuPciIds", "showSystemTray", "showClock",
"showNotificationButton", "showBattery", "showControlCenterButton",
"controlCenterShowNetworkIcon", "controlCenterShowBluetoothIcon", "controlCenterShowAudioIcon",
"controlCenterWidgets", "showWorkspaceIndex", "workspaceScrolling", "showWorkspacePadding", "showWorkspaceApps",
"maxWorkspaceIcons", "workspacesPerMonitor", "workspaceNameIcons", "waveProgressEnabled",
"clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode",
"runningAppsCurrentWorkspace", "runningAppsGroupByApp", "clockDateFormat", "lockDateFormat", "mediaSize",
"dankBarLeftWidgets", "dankBarCenterWidgets", "dankBarRightWidgets",
"appLauncherViewMode", "spotlightModalViewMode", "sortAppsAlphabetically",
"networkPreference", "iconTheme", "launcherLogoMode", "launcherLogoCustomPath",
"launcherLogoColorOverride", "launcherLogoColorInvertOnMode", "launcherLogoBrightness",
"launcherLogoContrast", "launcherLogoSizeOffset", "fontFamily", "monoFontFamily",
"fontWeight", "fontScale", "dankBarFontScale", "notepadUseMonospace",
"notepadFontFamily", "notepadFontSize", "notepadShowLineNumbers",
"notepadTransparencyOverride", "notepadLastCustomTransparency", "soundsEnabled",
"useSystemSoundTheme", "soundNewNotification", "soundVolumeChanged", "soundPluggedIn", "gtkThemingEnabled",
"qtThemingEnabled", "syncModeWithPortal", "showDock", "dockAutoHide", "dockGroupByApp",
"dockOpenOnOverview", "dockPosition", "dockSpacing", "dockBottomGap", "dockIconSize", "dockIndicatorStyle",
"cornerRadius", "notificationOverlayEnabled", "dankBarAutoHide",
"dankBarOpenOnOverview", "dankBarVisible", "dankBarSpacing", "dankBarBottomGap",
"dankBarInnerPadding", "dankBarSquareCorners", "dankBarNoBackground",
"dankBarGothCornersEnabled", "dankBarBorderEnabled", "dankBarBorderColor",
"dankBarBorderOpacity", "dankBarBorderThickness", "popupGapsAuto", "popupGapsManual",
"dankBarPosition", "lockScreenShowPowerActions", "enableFprint", "maxFprintTries",
"hideBrightnessSlider", "widgetBackgroundColor", "surfaceBase", "wallpaperFillMode",
"blurredWallpaperLayer", "blurWallpaperOnOverview", "notificationTimeoutLow", "notificationTimeoutNormal", "notificationTimeoutCritical",
"notificationPopupPosition", "osdAlwaysShowValue", "powerActionConfirm",
"customPowerActionLock", "customPowerActionLogout", "customPowerActionSuspend",
"customPowerActionHibernate", "customPowerActionReboot", "customPowerActionPowerOff",
"updaterUseCustomCommand", "updaterCustomCommand", "updaterTerminalAdditionalParams",
"screenPreferences", "showOnLastDisplay", "animationSpeed", "customAnimationDuration", "acMonitorTimeout", "acLockTimeout",
"acSuspendTimeout", "acHibernateTimeout", "batteryMonitorTimeout", "batteryLockTimeout",
"batterySuspendTimeout", "batteryHibernateTimeout", "lockBeforeSuspend",
"loginctlLockIntegration", "launchPrefix", "brightnessDevicePins", "configVersion"
]
try { try {
const content = settingsFile.text() const content = settingsFile.text()
if (!content || !content.trim()) return if (!content || !content.trim())
return
const settings = JSON.parse(content) const settings = JSON.parse(content)
let needsSave = false let needsSave = false
@@ -785,7 +809,7 @@ Singleton {
if (use24HourClock) { if (use24HourClock) {
return showSeconds ? "hh:mm:ss" : "hh:mm" return showSeconds ? "hh:mm:ss" : "hh:mm"
} else { } else {
return showSeconds ? "h:mm:ss AP": "h:mm AP" return showSeconds ? "h:mm:ss AP" : "h:mm AP"
} }
} }
@@ -896,10 +920,12 @@ Singleton {
PortalService.setSystemIconTheme(gtkThemeName) PortalService.setSystemIconTheme(gtkThemeName)
} }
var configScript = "mkdir -p " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0\n" + "\n" + "for config_dir in " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0; do\n" var configScript = "mkdir -p " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0\n" + "\n" + "for config_dir in " + _configDir + "/gtk-3.0 "
+ " settings_file=\"$config_dir/settings.ini\"\n" + " if [ -f \"$settings_file\" ]; then\n" + " if grep -q '^gtk-icon-theme-name=' \"$settings_file\"; then\n" + " sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=" + gtkThemeName + "/' \"$settings_file\"\n" + " else\n" + _configDir + "/gtk-4.0; do\n" + " settings_file=\"$config_dir/settings.ini\"\n" + " if [ -f \"$settings_file\" ]; then\n"
+ " if grep -q '\\[Settings\\]' \"$settings_file\"; then\n" + " sed -i '/\\[Settings\\]/a gtk-icon-theme-name=" + gtkThemeName + "' \"$settings_file\"\n" + " else\n" + " echo -e '\\n[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' >> \"$settings_file\"\n" + " fi\n" + " if grep -q '^gtk-icon-theme-name=' \"$settings_file\"; then\n" + " sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=" + gtkThemeName + "/' \"$settings_file\"\n"
+ " fi\n" + " else\n" + " echo -e '[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' > \"$settings_file\"\n" + " fi\n" + "done\n" + "\n" + "rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true\n" + "pkill -HUP -f 'gtk' 2>/dev/null || true\n" + " else\n" + " if grep -q '\\[Settings\\]' \"$settings_file\"; then\n" + " sed -i '/\\[Settings\\]/a gtk-icon-theme-name=" + gtkThemeName
+ "' \"$settings_file\"\n" + " else\n" + " echo -e '\\n[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' >> \"$settings_file\"\n" + " fi\n" + " fi\n" + " else\n"
+ " echo -e '[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' > \"$settings_file\"\n" + " fi\n" + "done\n" + "\n" + "rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true\n" + "pkill -HUP -f 'gtk' 2>/dev/null || true\n"
Quickshell.execDetached(["sh", "-lc", configScript]) Quickshell.execDetached(["sh", "-lc", configScript])
} }
} }
@@ -911,9 +937,11 @@ Singleton {
return return
} }
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" 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" + " 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"
+ " 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( + " 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"
qtThemeName) + "\n" + "update_qt_icon_theme " + _configDir + "/qt6ct/qt6ct.conf " + _shq(qtThemeName) + "\n" + "rm -rf " + home + "/.cache/icon-cache " + home + "/.cache/thumbnails 2>/dev/null || true\n" + " else\n" + " printf '\\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]) Quickshell.execDetached(["sh", "-lc", script])
} }
@@ -935,15 +963,15 @@ Singleton {
if (dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right) { if (dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right) {
return { return {
x: relativeY, "x": relativeY,
y: barThickness + dankBarSpacing + Theme.popupDistance, "y": barThickness + dankBarSpacing + Theme.popupDistance,
width: widgetWidth "width": widgetWidth
} }
} }
return { return {
x: relativeX, "x": relativeX,
y: barThickness + dankBarSpacing + dankBarBottomGap + Theme.popupDistance, "y": barThickness + dankBarSpacing + Theme.popupDistance,
width: widgetWidth "width": widgetWidth
} }
} }
@@ -966,11 +994,7 @@ Singleton {
} }
function sendTestNotification(index) { function sendTestNotification(index) {
const notifications = [ const notifications = [["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"], ["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"], ["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]]
["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"],
["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"],
["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]
]
if (index < 0 || index >= notifications.length) { if (index < 0 || index >= notifications.length) {
return return
@@ -1020,6 +1044,18 @@ Singleton {
} }
} }
function setMatugenTargetMonitor(monitorName) {
if (matugenTargetMonitor === monitorName)
return
matugenTargetMonitor = monitorName
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setDankBarTransparency(transparency) { function setDankBarTransparency(transparency) {
dankBarTransparency = transparency dankBarTransparency = transparency
saveSettings() saveSettings()
@@ -1239,6 +1275,11 @@ Singleton {
saveSettings() saveSettings()
} }
function setDwlShowAllTags(enabled) {
dwlShowAllTags = enabled
saveSettings()
}
function setWorkspaceNameIcon(workspaceName, iconData) { function setWorkspaceNameIcon(workspaceName, iconData) {
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons)) var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
iconMap[workspaceName] = iconData iconMap[workspaceName] = iconData
@@ -1279,6 +1320,11 @@ Singleton {
saveSettings() saveSettings()
} }
function setKeyboardLayoutNameCompactMode(enabled) {
keyboardLayoutNameCompactMode = enabled
saveSettings()
}
function setRunningAppsCurrentWorkspace(enabled) { function setRunningAppsCurrentWorkspace(enabled) {
runningAppsCurrentWorkspace = enabled runningAppsCurrentWorkspace = enabled
saveSettings() saveSettings()
@@ -1391,6 +1437,11 @@ Singleton {
saveSettings() saveSettings()
} }
function setVpnLastConnected(uuid) {
vpnLastConnected = uuid
saveSettings()
}
function setIconTheme(themeName) { function setIconTheme(themeName) {
iconTheme = themeName iconTheme = themeName
updateGtkIconTheme(themeName) updateGtkIconTheme(themeName)
@@ -1782,53 +1833,53 @@ Singleton {
} }
function setPowerActionConfirm(confirm) { function setPowerActionConfirm(confirm) {
powerActionConfirm = confirm; powerActionConfirm = confirm
saveSettings(); saveSettings()
} }
function setCustomPowerActionLock(command) { function setCustomPowerActionLock(command) {
customPowerActionLock = command; customPowerActionLock = command
saveSettings(); saveSettings()
} }
function setCustomPowerActionLogout(command) { function setCustomPowerActionLogout(command) {
customPowerActionLogout = command; customPowerActionLogout = command
saveSettings(); saveSettings()
} }
function setCustomPowerActionSuspend(command) { function setCustomPowerActionSuspend(command) {
customPowerActionSuspend = command; customPowerActionSuspend = command
saveSettings(); saveSettings()
} }
function setCustomPowerActionHibernate(command) { function setCustomPowerActionHibernate(command) {
customPowerActionHibernate = command; customPowerActionHibernate = command
saveSettings(); saveSettings()
} }
function setCustomPowerActionReboot(command) { function setCustomPowerActionReboot(command) {
customPowerActionReboot = command; customPowerActionReboot = command
saveSettings(); saveSettings()
} }
function setCustomPowerActionPowerOff(command) { function setCustomPowerActionPowerOff(command) {
customPowerActionPowerOff = command; customPowerActionPowerOff = command
saveSettings(); saveSettings()
} }
function setUpdaterUseCustomCommandEnabled(enabled) { function setUpdaterUseCustomCommandEnabled(enabled) {
updaterUseCustomCommand = enabled; updaterUseCustomCommand = enabled
saveSettings(); saveSettings()
} }
function setUpdaterCustomCommand(command) { function setUpdaterCustomCommand(command) {
updaterCustomCommand = command; updaterCustomCommand = command
saveSettings(); saveSettings()
} }
function setUpdaterTerminalAdditionalParams(customArgs) { function setUpdaterTerminalAdditionalParams(customArgs) {
updaterTerminalAdditionalParams = customArgs; updaterTerminalAdditionalParams = customArgs
saveSettings(); saveSettings()
} }
function setScreenPreferences(prefs) { function setScreenPreferences(prefs) {
@@ -1884,27 +1935,6 @@ Singleton {
id: rightWidgetsModel id: rightWidgetsModel
} }
Timer {
id: fontCheckTimer
interval: 3000
repeat: false
onTriggered: {
var availableFonts = Qt.fontFamilies()
var missingFonts = []
if (fontFamily === defaultFontFamily && !availableFonts.includes(defaultFontFamily))
missingFonts.push(defaultFontFamily)
if (monoFontFamily === defaultMonoFontFamily && !availableFonts.includes(defaultMonoFontFamily))
missingFonts.push(defaultMonoFontFamily)
if (missingFonts.length > 0) {
var message = "Missing fonts: " + missingFonts.join(", ") + ". Using system defaults."
ToastService.showWarning(message)
}
}
}
property Process testNotificationProcess property Process testNotificationProcess
testNotificationProcess: Process { testNotificationProcess: Process {

View File

@@ -19,7 +19,8 @@ Singleton {
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true" readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
readonly property real popupDistance: { readonly property real popupDistance: {
if (typeof SettingsData === "undefined") return 4 if (typeof SettingsData === "undefined")
return 4
return SettingsData.popupGapsAuto ? Math.max(4, SettingsData.dankBarSpacing) : SettingsData.popupGapsManual return SettingsData.popupGapsAuto ? Math.max(4, SettingsData.dankBarSpacing) : SettingsData.popupGapsManual
} }
@@ -29,13 +30,14 @@ Singleton {
property bool colorsFileLoadFailed: false property bool colorsFileLoadFailed: false
readonly property string dynamic: "dynamic" readonly property string dynamic: "dynamic"
readonly property string custom : "custom" readonly property string custom: "custom"
readonly property string homeDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation)) readonly property string homeDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation))
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "") readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "")
readonly property string wallpaperPath: { readonly property string wallpaperPath: {
if (typeof SessionData === "undefined") return "" if (typeof SessionData === "undefined")
return ""
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
var screens = Quickshell.screens var screens = Quickshell.screens
@@ -60,14 +62,28 @@ Singleton {
return wallpaperPath return wallpaperPath
} }
readonly property string rawWallpaperPath: { readonly property string rawWallpaperPath: {
if (typeof SessionData === "undefined") return "" if (typeof SessionData === "undefined")
return ""
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
// Use first monitor's wallpaper for dynamic theming
var screens = Quickshell.screens var screens = Quickshell.screens
if (screens.length > 0) { if (screens.length > 0) {
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name) var targetMonitor = (typeof SettingsData !== "undefined" && SettingsData.matugenTargetMonitor && SettingsData.matugenTargetMonitor !== "") ? SettingsData.matugenTargetMonitor : screens[0].name
return firstMonitorWallpaper || SessionData.wallpaperPath
var targetMonitorExists = false
for (var i = 0; i < screens.length; i++) {
if (screens[i].name === targetMonitor) {
targetMonitorExists = true
break
}
}
if (!targetMonitorExists) {
targetMonitor = screens[0].name
}
var targetMonitorWallpaper = SessionData.getMonitorWallpaper(targetMonitor)
return targetMonitorWallpaper || SessionData.wallpaperPath
} }
} }
@@ -91,16 +107,16 @@ Singleton {
return return
} }
if (colorsFileLoadFailed && currentTheme === dynamic && wallpaperPath) { if (colorsFileLoadFailed && currentTheme === dynamic && rawWallpaperPath) {
console.info("Theme: Matugen now available, regenerating colors for dynamic theme") console.info("Theme: Matugen now available, regenerating colors for dynamic theme")
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default" const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot" const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
if (wallpaperPath.startsWith("#")) { const effectivePath = rawWallpaperPath.startsWith("we:") ? (stateDir + "/we_screenshots/" + rawWallpaperPath.substring(3) + ".jpg") : rawWallpaperPath
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType) if (effectivePath.startsWith("#")) {
setDesiredTheme("hex", effectivePath, isLight, iconTheme, selectedMatugenType)
} else { } else {
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType) setDesiredTheme("image", effectivePath, isLight, iconTheme, selectedMatugenType)
} }
return return
} }
@@ -109,13 +125,13 @@ Singleton {
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default" const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
if (currentTheme === dynamic) { if (currentTheme === dynamic) {
if (wallpaperPath) { if (rawWallpaperPath) {
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot" const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
if (wallpaperPath.startsWith("#")) { const effectivePath = rawWallpaperPath.startsWith("we:") ? (stateDir + "/we_screenshots/" + rawWallpaperPath.substring(3) + ".jpg") : rawWallpaperPath
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType) if (effectivePath.startsWith("#")) {
setDesiredTheme("hex", effectivePath, isLight, iconTheme, selectedMatugenType)
} else { } else {
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType) setDesiredTheme("image", effectivePath, isLight, iconTheme, selectedMatugenType)
} }
} }
} else { } else {
@@ -132,7 +148,6 @@ Singleton {
} }
if (primaryColor) { if (primaryColor) {
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType) setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
} }
} }
@@ -194,22 +209,51 @@ Singleton {
} }
} }
readonly property var availableMatugenSchemes: [ readonly property var availableMatugenSchemes: [({
({ "value": "scheme-tonal-spot", "label": "Tonal Spot", "description": I18n.tr("Balanced palette with focused accents (default).") }), "value": "scheme-tonal-spot",
({ "value": "scheme-vibrant-spot", "label": "Vibrant Spot", "description": I18n.tr("Lively palette with saturated accents.") }), "label": "Tonal Spot",
({ "value": "scheme-dynamic-contrast", "label": "Dynamic Contrast", "description": I18n.tr("High-contrast palette for strong visual distinction.") }), "description": I18n.tr("Balanced palette with focused accents (default).")
({ "value": "scheme-content", "label": "Content", "description": I18n.tr("Derives colors that closely match the underlying image.") }), }), ({
({ "value": "scheme-expressive", "label": "Expressive", "description": I18n.tr("Vibrant palette with playful saturation.") }), "value": "scheme-vibrant-spot",
({ "value": "scheme-fidelity", "label": "Fidelity", "description": I18n.tr("High-fidelity palette that preserves source hues.") }), "label": "Vibrant Spot",
({ "value": "scheme-fruit-salad", "label": "Fruit Salad", "description": I18n.tr("Colorful mix of bright contrasting accents.") }), "description": I18n.tr("Lively palette with saturated accents.")
({ "value": "scheme-monochrome", "label": "Monochrome", "description": I18n.tr("Minimal palette built around a single hue.") }), }), ({
({ "value": "scheme-neutral", "label": "Neutral", "description": I18n.tr("Muted palette with subdued, calming tones.") }), "value": "scheme-dynamic-contrast",
({ "value": "scheme-rainbow", "label": "Rainbow", "description": I18n.tr("Diverse palette spanning the full spectrum.") }) "label": "Dynamic Contrast",
] "description": I18n.tr("High-contrast palette for strong visual distinction.")
}), ({
"value": "scheme-content",
"label": "Content",
"description": I18n.tr("Derives colors that closely match the underlying image.")
}), ({
"value": "scheme-expressive",
"label": "Expressive",
"description": I18n.tr("Vibrant palette with playful saturation.")
}), ({
"value": "scheme-fidelity",
"label": "Fidelity",
"description": I18n.tr("High-fidelity palette that preserves source hues.")
}), ({
"value": "scheme-fruit-salad",
"label": "Fruit Salad",
"description": I18n.tr("Colorful mix of bright contrasting accents.")
}), ({
"value": "scheme-monochrome",
"label": "Monochrome",
"description": I18n.tr("Minimal palette built around a single hue.")
}), ({
"value": "scheme-neutral",
"label": "Neutral",
"description": I18n.tr("Muted palette with subdued, calming tones.")
}), ({
"value": "scheme-rainbow",
"label": "Rainbow",
"description": I18n.tr("Diverse palette spanning the full spectrum.")
})]
function getMatugenScheme(value) { function getMatugenScheme(value) {
const schemes = availableMatugenSchemes const schemes = availableMatugenSchemes
for (let i = 0; i < schemes.length; i++) { for (var i = 0; i < schemes.length; i++) {
if (schemes[i].value === value) if (schemes[i].value === value)
return schemes[i] return schemes[i]
} }
@@ -296,13 +340,37 @@ Singleton {
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08) property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3) property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
readonly property var animationDurations: [ readonly property var animationDurations: [{
{ shorter: 0, short: 0, medium: 0, long: 0, extraLong: 0 }, "shorter": 0,
{ shorter: 50, short: 75, medium: 150, long: 250, extraLong: 500 }, "short": 0,
{ shorter: 100, short: 150, medium: 300, long: 500, extraLong: 1000 }, "medium": 0,
{ shorter: 150, short: 225, medium: 450, long: 750, extraLong: 1500 }, "long": 0,
{ shorter: 200, short: 300, medium: 600, long: 1000, extraLong: 2000 } "extraLong": 0
] }, {
"shorter": 50,
"short": 75,
"medium": 150,
"long": 250,
"extraLong": 500
}, {
"shorter": 100,
"short": 150,
"medium": 300,
"long": 500,
"extraLong": 1000
}, {
"shorter": 150,
"short": 225,
"medium": 450,
"long": 750,
"extraLong": 1500
}, {
"shorter": 200,
"short": 300,
"medium": 600,
"long": 1000,
"extraLong": 2000
}]
readonly property int currentAnimationSpeed: typeof SettingsData !== "undefined" ? SettingsData.animationSpeed : SettingsData.AnimationSpeed.Short readonly property int currentAnimationSpeed: typeof SettingsData !== "undefined" ? SettingsData.animationSpeed : SettingsData.AnimationSpeed.Short
readonly property var currentDurations: animationDurations[currentAnimationSpeed] || animationDurations[SettingsData.AnimationSpeed.Short] readonly property var currentDurations: animationDurations[currentAnimationSpeed] || animationDurations[SettingsData.AnimationSpeed.Short]
@@ -335,7 +403,8 @@ Singleton {
} }
readonly property int currentAnimationBaseDuration: { readonly property int currentAnimationBaseDuration: {
if (typeof SettingsData === "undefined") return 500 if (typeof SettingsData === "undefined")
return 500
if (SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) { if (SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) {
return SettingsData.customAnimationDuration return SettingsData.customAnimationDuration
@@ -485,20 +554,40 @@ Singleton {
function getCatppuccinColor(variantName) { function getCatppuccinColor(variantName) {
const catColors = { const catColors = {
"cat-rosewater": "#f5e0dc", "cat-flamingo": "#f2cdcd", "cat-pink": "#f5c2e7", "cat-mauve": "#cba6f7", "cat-rosewater": "#f5e0dc",
"cat-red": "#f38ba8", "cat-maroon": "#eba0ac", "cat-peach": "#fab387", "cat-yellow": "#f9e2af", "cat-flamingo": "#f2cdcd",
"cat-green": "#a6e3a1", "cat-teal": "#94e2d5", "cat-sky": "#89dceb", "cat-sapphire": "#74c7ec", "cat-pink": "#f5c2e7",
"cat-blue": "#89b4fa", "cat-lavender": "#b4befe" "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" return catColors[variantName] || "#cba6f7"
} }
function getCatppuccinVariantName(variantName) { function getCatppuccinVariantName(variantName) {
const catNames = { const catNames = {
"cat-rosewater": "Rosewater", "cat-flamingo": "Flamingo", "cat-pink": "Pink", "cat-mauve": "Mauve", "cat-rosewater": "Rosewater",
"cat-red": "Red", "cat-maroon": "Maroon", "cat-peach": "Peach", "cat-yellow": "Yellow", "cat-flamingo": "Flamingo",
"cat-green": "Green", "cat-teal": "Teal", "cat-sky": "Sky", "cat-sapphire": "Sapphire", "cat-pink": "Pink",
"cat-blue": "Blue", "cat-lavender": "Lavender" "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" return catNames[variantName] || "Unknown"
} }
@@ -593,8 +682,10 @@ Singleton {
function barTextSize(barThickness) { function barTextSize(barThickness) {
const scale = barThickness / 48 const scale = barThickness / 48
const dankBarScale = (typeof SettingsData !== "undefined" ? SettingsData.dankBarFontScale : 1.0) const dankBarScale = (typeof SettingsData !== "undefined" ? SettingsData.dankBarFontScale : 1.0)
if (scale <= 0.75) return fontSizeSmall * 0.9 * dankBarScale if (scale <= 0.75)
if (scale >= 1.25) return fontSizeMedium * dankBarScale return fontSizeSmall * 0.9 * dankBarScale
if (scale >= 1.25)
return fontSizeMedium * dankBarScale
return fontSizeSmall * dankBarScale return fontSizeSmall * dankBarScale
} }
@@ -688,7 +779,6 @@ Singleton {
} }
} }
function onLightModeChanged() { function onLightModeChanged() {
if (currentTheme === "custom" && customThemeFileView.path) { if (currentTheme === "custom" && customThemeFileView.path) {
customThemeFileView.reload() customThemeFileView.reload()
@@ -721,14 +811,12 @@ Singleton {
const desiredPath = stateDir + "/matugen.desired.json" 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`])
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
workerRunning = true workerRunning = true
const syncModeWithPortal = (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal) ? "true" : "false" const syncModeWithPortal = (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal) ? "true" : "false"
if (rawWallpaperPath.startsWith("we:")) { if (rawWallpaperPath.startsWith("we:")) {
console.log("Theme: Starting matugen worker (WE wallpaper)") console.log("Theme: Starting matugen worker (WE wallpaper, waiting for screenshot)")
systemThemeGenerator.command = [ systemThemeGenerator.command = ["sh", "-c", `sleep 3 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' '${configDir}' '${syncModeWithPortal}' --run`]
"sh", "-c",
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' '${configDir}' '${syncModeWithPortal}' --run`
]
} else { } else {
console.log("Theme: Starting matugen worker") console.log("Theme: Starting matugen worker")
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, configDir, syncModeWithPortal, "--run"] systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, configDir, syncModeWithPortal, "--run"]
@@ -745,14 +833,15 @@ Singleton {
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default" const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
if (currentTheme === dynamic) { if (currentTheme === dynamic) {
if (!wallpaperPath) { if (!rawWallpaperPath) {
return return
} }
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot" const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
if (wallpaperPath.startsWith("#")) { const effectivePath = rawWallpaperPath.startsWith("we:") ? (stateDir + "/we_screenshots/" + rawWallpaperPath.substring(3) + ".jpg") : rawWallpaperPath
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType) if (effectivePath.startsWith("#")) {
setDesiredTheme("hex", effectivePath, isLight, iconTheme, selectedMatugenType)
} else { } else {
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType) setDesiredTheme("image", effectivePath, isLight, iconTheme, selectedMatugenType)
} }
} else { } else {
let primaryColor let primaryColor
@@ -820,20 +909,30 @@ Singleton {
}) })
} }
function withAlpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a); } function withAlpha(c, a) {
return Qt.rgba(c.r, c.g, c.b, a)
}
function getFillMode(modeName) { function getFillMode(modeName) {
switch(modeName) { switch (modeName) {
case "Stretch": return Image.Stretch case "Stretch":
return Image.Stretch
case "Fit": case "Fit":
case "PreserveAspectFit": return Image.PreserveAspectFit case "PreserveAspectFit":
return Image.PreserveAspectFit
case "Fill": case "Fill":
case "PreserveAspectCrop": return Image.PreserveAspectCrop case "PreserveAspectCrop":
case "Tile": return Image.Tile return Image.PreserveAspectCrop
case "TileVertically": return Image.TileVertically case "Tile":
case "TileHorizontally": return Image.TileHorizontally return Image.Tile
case "Pad": return Image.Pad case "TileVertically":
default: return Image.PreserveAspectCrop return Image.TileVertically
case "TileHorizontally":
return Image.TileHorizontally
case "Pad":
return Image.Pad
default:
return Image.PreserveAspectCrop
} }
} }
@@ -852,40 +951,48 @@ Singleton {
} }
function invertHex(hex) { function invertHex(hex) {
hex = hex.replace('#', ''); hex = hex.replace('#', '')
if (!/^[0-9A-Fa-f]{6}$/.test(hex)) { if (!/^[0-9A-Fa-f]{6}$/.test(hex)) {
return hex; return hex
} }
const r = parseInt(hex.substr(0, 2), 16); const r = parseInt(hex.substr(0, 2), 16)
const g = parseInt(hex.substr(2, 2), 16); const g = parseInt(hex.substr(2, 2), 16)
const b = parseInt(hex.substr(4, 2), 16); const b = parseInt(hex.substr(4, 2), 16)
const invR = (255 - r).toString(16).padStart(2, '0'); const invR = (255 - r).toString(16).padStart(2, '0')
const invG = (255 - g).toString(16).padStart(2, '0'); const invG = (255 - g).toString(16).padStart(2, '0')
const invB = (255 - b).toString(16).padStart(2, '0'); const invB = (255 - b).toString(16).padStart(2, '0')
return `#${invR}${invG}${invB}`; return `#${invR}${invG}${invB}`
} }
property string baseLogoColor: { property string baseLogoColor: {
if (typeof SettingsData === "undefined") return "" if (typeof SettingsData === "undefined")
return ""
const colorOverride = SettingsData.launcherLogoColorOverride const colorOverride = SettingsData.launcherLogoColorOverride
if (!colorOverride || colorOverride === "") return "" if (!colorOverride || colorOverride === "")
if (colorOverride === "primary") return primary return ""
if (colorOverride === "surface") return surfaceText if (colorOverride === "primary")
return primary
if (colorOverride === "surface")
return surfaceText
return colorOverride return colorOverride
} }
property string effectiveLogoColor: { property string effectiveLogoColor: {
if (typeof SettingsData === "undefined") return "" if (typeof SettingsData === "undefined")
return ""
const colorOverride = SettingsData.launcherLogoColorOverride const colorOverride = SettingsData.launcherLogoColorOverride
if (!colorOverride || colorOverride === "") return "" if (!colorOverride || colorOverride === "")
return ""
if (colorOverride === "primary") return primary if (colorOverride === "primary")
if (colorOverride === "surface") return surfaceText return primary
if (colorOverride === "surface")
return surfaceText
if (!SettingsData.launcherLogoColorInvertOnMode) { if (!SettingsData.launcherLogoColorInvertOnMode) {
return colorOverride return colorOverride
@@ -898,8 +1005,6 @@ Singleton {
return colorOverride return colorOverride
} }
Process { Process {
id: systemThemeGenerator id: systemThemeGenerator
running: false running: false
@@ -956,9 +1061,7 @@ Singleton {
id: dynamicColorsFileView id: dynamicColorsFileView
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms" const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
const colorsPath = SessionData.isGreeterMode const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json"
? greetCfgDir + "/colors.json"
: stateDir + "/dms-colors.json"
return colorsPath return colorsPath
} }
watchChanges: currentTheme === dynamic && !SessionData.isGreeterMode watchChanges: currentTheme === dynamic && !SessionData.isGreeterMode
@@ -1000,7 +1103,7 @@ Singleton {
console.warn("Theme: Dynamic colors file load failed, marking for regeneration") console.warn("Theme: Dynamic colors file load failed, marking for regeneration")
colorsFileLoadFailed = true colorsFileLoadFailed = true
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode) const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
if (!isGreeterMode && matugenAvailable && wallpaperPath) { if (!isGreeterMode && matugenAvailable && rawWallpaperPath) {
console.log("Theme: Matugen available, triggering immediate regeneration") console.log("Theme: Matugen available, triggering immediate regeneration")
generateSystemThemesFromCurrentTheme() generateSystemThemesFromCurrentTheme()
} }

View File

@@ -54,7 +54,7 @@ Item {
Loader { Loader {
id: blurredWallpaperBackgroundLoader id: blurredWallpaperBackgroundLoader
active: SettingsData.blurredWallpaperLayer active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
asynchronous: false asynchronous: false
sourceComponent: BlurredWallpaperBackground {} sourceComponent: BlurredWallpaperBackground {}
@@ -213,10 +213,26 @@ Item {
} }
} }
property string lastCredentialsToken: ""
property var lastCredentialsTime: 0
Connections { Connections {
target: NetworkService target: NetworkService
function onCredentialsNeeded(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) { function onCredentialsNeeded(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
const now = Date.now()
const timeSinceLastPrompt = now - lastCredentialsTime
if (wifiPasswordModal.shouldBeVisible && timeSinceLastPrompt < 1000) {
NetworkService.cancelCredentials(lastCredentialsToken)
lastCredentialsToken = token
lastCredentialsTime = now
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService)
return
}
lastCredentialsToken = token
lastCredentialsTime = now
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService)
} }
} }

View File

@@ -135,24 +135,22 @@ Item {
} }
function toggle(tab: string): string { function toggle(tab: string): string {
root.dankDashPopoutLoader.active = true if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
if (root.dankDashPopoutLoader.item) { if (root.dankDashPopoutLoader.item) {
if (root.dankDashPopoutLoader.item.dashVisible) {
root.dankDashPopoutLoader.item.dashVisible = false
} else {
switch (tab.toLowerCase()) { switch (tab.toLowerCase()) {
case "media": case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1 root.dankDashPopoutLoader.item.currentTabIndex = 1
break break
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2
break
case "weather": case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0 root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0
break break
default: default:
root.dankDashPopoutLoader.item.currentTabIndex = 0 root.dankDashPopoutLoader.item.currentTabIndex = 0
break break
} }
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
root.dankDashPopoutLoader.item.dashVisible = true
} }
return "DASH_TOGGLE_SUCCESS" return "DASH_TOGGLE_SUCCESS"
} }

View File

@@ -17,17 +17,7 @@ PanelWindow {
property real height: 300 property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920 readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080 readonly property real screenHeight: screen ? screen.height : 1080
readonly property real dpr: { readonly property real dpr: CompositorService.getScreenScale(screen)
if (CompositorService.isNiri && screen) {
const niriScale = NiriService.displayScales[screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return (screen?.devicePixelRatio) || 1
}
property bool showBackground: true property bool showBackground: true
property real backgroundOpacity: 0.5 property real backgroundOpacity: 0.5
property string positioning: "center" property string positioning: "center"

View File

@@ -45,6 +45,12 @@ DankModal {
close() close()
} }
function hideInstant() {
onColorSelectedCallback = null
shouldBeVisible = false
visible = false
}
onColorSelected: (color) => { onColorSelected: (color) => {
if (onColorSelectedCallback) { if (onColorSelectedCallback) {
onColorSelectedCallback(color) onColorSelectedCallback(color)
@@ -78,7 +84,7 @@ DankModal {
} }
function pickColorFromScreen() { function pickColorFromScreen() {
hide() hideInstant()
Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (output, errorCode) => { Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (output, errorCode) => {
if (errorCode !== 0) { if (errorCode !== 0) {
console.warn("hyprpicker exited with code:", errorCode) console.warn("hyprpicker exited with code:", errorCode)

View File

@@ -0,0 +1,204 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Widgets
StyledRect {
id: delegateRoot
required property bool fileIsDir
required property string filePath
required property string fileName
required property int index
property bool weMode: false
property var iconSizes: [80, 120, 160, 200]
property int iconSizeIndex: 1
property int selectedIndex: -1
property bool keyboardNavigationActive: false
signal itemClicked(int index, string path, string name, bool isDir)
signal itemSelected(int index, string path, string name, bool isDir)
function getFileExtension(fileName) {
const parts = fileName.split('.')
if (parts.length > 1) {
return parts[parts.length - 1].toLowerCase()
}
return ""
}
function determineFileType(fileName) {
const ext = getFileExtension(fileName)
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]
if (imageExts.includes(ext)) {
return "image"
}
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]
if (videoExts.includes(ext)) {
return "video"
}
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]
if (audioExts.includes(ext)) {
return "audio"
}
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]
if (codeExts.includes(ext)) {
return "code"
}
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]
if (docExts.includes(ext)) {
return "document"
}
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]
if (archiveExts.includes(ext)) {
return "archive"
}
if (!ext || fileName.indexOf('.') === -1) {
return "binary"
}
return "file"
}
function isImageFile(fileName) {
if (!fileName) {
return false
}
return determineFileType(fileName) === "image"
}
function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase()
if (lowerName.startsWith("dockerfile")) {
return "docker"
}
const ext = fileName.split('.').pop()
return ext || ""
}
width: weMode ? 245 : iconSizes[iconSizeIndex] + 16
height: weMode ? 205 : iconSizes[iconSizeIndex] + 48
radius: Theme.cornerRadius
color: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
return Theme.surfacePressed
return mouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
}
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : "transparent"
border.width: (keyboardNavigationActive && delegateRoot.index === selectedIndex) ? 2 : 0
Component.onCompleted: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
onSelectedIndexChanged: {
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
width: weMode ? 225 : (iconSizes[iconSizeIndex] - 8)
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
anchors.horizontalCenter: parent.horizontalCenter
CachingImage {
id: gridPreviewImage
anchors.fill: parent
anchors.margins: 2
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
property int weExtIndex: 0
source: {
if (weMode && delegateRoot.fileIsDir) {
return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
}
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
}
onStatusChanged: {
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
if (weExtIndex < weExtensions.length - 1) {
weExtIndex++
source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
} else {
source = ""
}
}
}
fillMode: Image.PreserveAspectCrop
maxCacheSize: weMode ? 225 : iconSizes[iconSizeIndex]
visible: false
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: gridPreviewImage
maskEnabled: true
maskSource: gridImageMask
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir))
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: gridImageMask
anchors.fill: parent
anchors.margins: 2
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: "black"
antialiasing: true
}
}
DankNFIcon {
anchors.centerIn: parent
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
size: iconSizes[iconSizeIndex] * 0.45
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
visible: (!delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)) || (delegateRoot.fileIsDir && !weMode)
}
}
StyledText {
text: delegateRoot.fileName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: delegateRoot.width - Theme.spacingM
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
maximumLineCount: 2
wrapMode: Text.Wrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
}
}

View File

@@ -0,0 +1,209 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Widgets
StyledRect {
id: listDelegateRoot
required property bool fileIsDir
required property string filePath
required property string fileName
required property int index
required property var fileModified
required property int fileSize
property int selectedIndex: -1
property bool keyboardNavigationActive: false
signal itemClicked(int index, string path, string name, bool isDir)
signal itemSelected(int index, string path, string name, bool isDir)
function getFileExtension(fileName) {
const parts = fileName.split('.')
if (parts.length > 1) {
return parts[parts.length - 1].toLowerCase()
}
return ""
}
function determineFileType(fileName) {
const ext = getFileExtension(fileName)
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]
if (imageExts.includes(ext)) {
return "image"
}
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]
if (videoExts.includes(ext)) {
return "video"
}
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]
if (audioExts.includes(ext)) {
return "audio"
}
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]
if (codeExts.includes(ext)) {
return "code"
}
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]
if (docExts.includes(ext)) {
return "document"
}
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]
if (archiveExts.includes(ext)) {
return "archive"
}
if (!ext || fileName.indexOf('.') === -1) {
return "binary"
}
return "file"
}
function isImageFile(fileName) {
if (!fileName) {
return false
}
return determineFileType(fileName) === "image"
}
function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase()
if (lowerName.startsWith("dockerfile")) {
return "docker"
}
const ext = fileName.split('.').pop()
return ext || ""
}
function formatFileSize(size) {
if (size < 1024)
return size + " B"
if (size < 1024 * 1024)
return (size / 1024).toFixed(1) + " KB"
if (size < 1024 * 1024 * 1024)
return (size / (1024 * 1024)).toFixed(1) + " MB"
return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB"
}
height: 44
radius: Theme.cornerRadius
color: {
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
return Theme.surfacePressed
return listMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
}
border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : "transparent"
border.width: (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) ? 2 : 0
Component.onCompleted: {
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir)
}
onSelectedIndexChanged: {
if (keyboardNavigationActive && selectedIndex === listDelegateRoot.index)
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir)
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
Item {
width: 28
height: 28
anchors.verticalCenter: parent.verticalCenter
CachingImage {
id: listPreviewImage
anchors.fill: parent
source: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? ("file://" + listDelegateRoot.filePath) : ""
fillMode: Image.PreserveAspectCrop
maxCacheSize: 32
visible: false
}
MultiEffect {
anchors.fill: parent
source: listPreviewImage
maskEnabled: true
maskSource: listImageMask
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: listImageMask
anchors.fill: parent
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: "black"
antialiasing: true
}
}
DankNFIcon {
anchors.centerIn: parent
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
size: Theme.iconSize - 2
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
visible: listDelegateRoot.fileIsDir || !isImageFile(listDelegateRoot.fileName)
}
}
StyledText {
text: listDelegateRoot.fileName || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width - 280
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
clip: true
}
StyledText {
text: listDelegateRoot.fileIsDir ? "" : formatFileSize(listDelegateRoot.fileSize)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: 70
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: Qt.formatDateTime(listDelegateRoot.fileModified, "MMM d, yyyy h:mm AP")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: 140
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: listMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,130 @@
import QtQuick
import qs.Common
import qs.Widgets
Row {
id: navigation
property string currentPath: ""
property string homeDir: ""
property bool backButtonFocused: false
property bool keyboardNavigationActive: false
property bool showSidebar: true
property bool pathEditMode: false
property bool pathInputHasFocus: false
signal navigateUp()
signal navigateTo(string path)
signal pathInputFocusChanged(bool hasFocus)
height: 40
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
spacing: Theme.spacingS
StyledRect {
width: 32
height: 32
radius: Theme.cornerRadius
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1 : 0
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: backButtonMouseArea
anchors.fill: parent
hoverEnabled: currentPath !== homeDir
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: currentPath !== homeDir
onClicked: navigation.navigateUp()
}
}
Item {
width: Math.max(0, (parent?.width ?? 0) - 40 - Theme.spacingS - (showSidebar ? 0 : 80))
height: 32
anchors.verticalCenter: parent.verticalCenter
StyledRect {
anchors.fill: parent
radius: Theme.cornerRadius
color: pathEditMode ? Theme.surfaceContainer : "transparent"
border.color: pathEditMode ? Theme.primary : "transparent"
border.width: pathEditMode ? 1 : 0
visible: !pathEditMode
StyledText {
id: pathDisplay
text: currentPath.replace("file://", "")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
elide: Text.ElideMiddle
verticalAlignment: Text.AlignVCenter
maximumLineCount: 1
wrapMode: Text.NoWrap
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
pathEditMode = true
pathInput.text = currentPath.replace("file://", "")
Qt.callLater(() => pathInput.forceActiveFocus())
}
}
}
DankTextField {
id: pathInput
anchors.fill: parent
visible: pathEditMode
topPadding: Theme.spacingXS
bottomPadding: Theme.spacingXS
onAccepted: {
const newPath = text.trim()
if (newPath !== "") {
navigation.navigateTo(newPath)
}
pathEditMode = false
}
Keys.onEscapePressed: {
pathEditMode = false
}
Keys.onDownPressed: {
pathEditMode = false
}
onActiveFocusChanged: {
navigation.pathInputFocusChanged(activeFocus)
if (!activeFocus && pathEditMode) {
pathEditMode = false
}
}
}
}
Row {
spacing: Theme.spacingXS
visible: !showSidebar
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
circular: false
iconName: "sort"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
}
}
}

View File

@@ -0,0 +1,127 @@
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: overwriteDialog
property bool showDialog: false
property string pendingFilePath: ""
signal confirmed(string filePath)
signal cancelled()
visible: showDialog
focus: showDialog
Keys.onEscapePressed: {
cancelled()
}
Keys.onReturnPressed: {
confirmed(pendingFilePath)
}
Rectangle {
anchors.fill: parent
color: Theme.shadowStrong
opacity: 0.8
MouseArea {
anchors.fill: parent
onClicked: {
cancelled()
}
}
}
StyledRect {
anchors.centerIn: parent
width: 400
height: 160
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 1
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingL * 2
spacing: Theme.spacingM
StyledText {
text: I18n.tr("File Already Exists")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("A file with this name already exists. Do you want to overwrite it?")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
StyledRect {
width: 80
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
StyledText {
anchors.centerIn: parent
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
cancelled()
}
}
}
StyledRect {
width: 90
height: 36
radius: Theme.cornerRadius
color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
StyledText {
anchors.centerIn: parent
text: I18n.tr("Overwrite")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: overwriteArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
confirmed(pendingFilePath)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
import QtQuick
import qs.Common
import qs.Widgets
Row {
id: saveRow
property bool saveMode: false
property string defaultFileName: ""
property string currentPath: ""
signal saveRequested(string filePath)
height: saveMode ? 40 : 0
visible: saveMode
spacing: Theme.spacingM
DankTextField {
id: fileNameInput
width: parent.width - saveButton.width - Theme.spacingM
height: 40
text: defaultFileName
placeholderText: I18n.tr("Enter filename...")
ignoreLeftRightKeys: false
focus: saveMode
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
Component.onCompleted: {
if (saveMode)
Qt.callLater(() => {
forceActiveFocus()
})
}
onAccepted: {
if (text.trim() !== "") {
var basePath = currentPath.replace(/^file:\/\//, '')
var fullPath = basePath + "/" + text.trim()
fullPath = fullPath.replace(/\/+/g, '/')
saveRequested(fullPath)
}
}
}
StyledRect {
id: saveButton
width: 80
height: 40
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: I18n.tr("Save")
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
}
StateLayer {
stateColor: Theme.primary
cornerRadius: Theme.cornerRadius
enabled: fileNameInput.text.trim() !== ""
onClicked: {
if (fileNameInput.text.trim() !== "") {
var basePath = currentPath.replace(/^file:\/\//, '')
var fullPath = basePath + "/" + fileNameInput.text.trim()
fullPath = fullPath.replace(/\/+/g, '/')
saveRequested(fullPath)
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: sidebar
property var quickAccessLocations: []
property string currentPath: ""
signal locationSelected(string path)
width: 200
color: Theme.surface
clip: true
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 4
StyledText {
text: "Quick Access"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
leftPadding: Theme.spacingS
bottomPadding: Theme.spacingXS
}
Repeater {
model: quickAccessLocations
StyledRect {
width: parent?.width ?? 0
height: 38
radius: Theme.cornerRadius
color: quickAccessMouseArea.containsMouse ? Theme.surfaceContainerHigh : (currentPath === modelData?.path ? Theme.surfacePressed : "transparent")
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: modelData?.icon ?? ""
size: Theme.iconSize - 2
color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData?.name ?? ""
font.pixelSize: Theme.fontSizeMedium
color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
font.weight: currentPath === modelData?.path ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: quickAccessMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: locationSelected(modelData?.path ?? "")
}
}
}
}
}

View File

@@ -0,0 +1,183 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: sortMenu
property string sortBy: "name"
property bool sortAscending: true
signal sortBySelected(string value)
signal sortOrderSelected(bool ascending)
width: 200
height: sortColumn.height + Theme.spacingM * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 1
visible: false
z: 100
Column {
id: sortColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: "Sort By"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
}
Repeater {
model: [{
"name": "Name",
"value": "name"
}, {
"name": "Size",
"value": "size"
}, {
"name": "Modified",
"value": "modified"
}, {
"name": "Type",
"value": "type"
}]
StyledRect {
width: sortColumn?.width ?? 0
height: 32
radius: Theme.cornerRadius
color: sortMouseArea.containsMouse ? Theme.surfaceVariant : (sortBy === modelData?.value ? Theme.surfacePressed : "transparent")
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: sortBy === modelData?.value ? "check" : ""
size: Theme.iconSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: sortBy === modelData?.value
}
StyledText {
text: modelData?.name ?? ""
font.pixelSize: Theme.fontSizeMedium
color: sortBy === modelData?.value ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: sortMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
sortMenu.sortBySelected(modelData?.value ?? "name")
sortMenu.visible = false
}
}
}
}
StyledRect {
width: sortColumn.width
height: 1
color: Theme.outline
}
StyledText {
text: "Order"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
topPadding: Theme.spacingXS
}
StyledRect {
width: sortColumn?.width ?? 0
height: 32
radius: Theme.cornerRadius
color: ascMouseArea.containsMouse ? Theme.surfaceVariant : (sortAscending ? Theme.surfacePressed : "transparent")
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: "arrow_upward"
size: Theme.iconSizeSmall
color: sortAscending ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Ascending"
font.pixelSize: Theme.fontSizeMedium
color: sortAscending ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: ascMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
sortMenu.sortOrderSelected(true)
sortMenu.visible = false
}
}
}
StyledRect {
width: sortColumn?.width ?? 0
height: 32
radius: Theme.cornerRadius
color: descMouseArea.containsMouse ? Theme.surfaceVariant : (!sortAscending ? Theme.surfacePressed : "transparent")
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: "arrow_downward"
size: Theme.iconSizeSmall
color: !sortAscending ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Descending"
font.pixelSize: Theme.fontSizeMedium
color: !sortAscending ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: descMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
sortMenu.sortOrderSelected(false)
sortMenu.visible = false
}
}
}
}
}

View File

@@ -34,8 +34,8 @@ DankModal {
} }
objectName: "settingsModal" objectName: "settingsModal"
width: 800 width: Math.min(800, screenWidth * 0.9)
height: 800 height: Math.min(800, screenHeight * 0.85)
visible: false visible: false
onBackgroundClicked: () => { onBackgroundClicked: () => {
return hide(); return hide();

View File

@@ -0,0 +1,237 @@
import QtQuick
import Quickshell.Io
import qs.Services
Item {
id: controller
property string searchQuery: ""
property alias model: fileModel
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property bool isSearching: false
property int totalResults: 0
property string searchField: "filename"
signal searchCompleted
ListModel {
id: fileModel
}
function performSearch() {
if (!DSearchService.dsearchAvailable) {
model.clear()
totalResults = 0
isSearching = false
return
}
if (searchQuery.length === 0) {
model.clear()
totalResults = 0
isSearching = false
return
}
isSearching = true
const params = {
"limit": 50,
"fuzzy": true,
"sort": "score",
"desc": true
}
if (searchField && searchField !== "all") {
params.field = searchField
}
DSearchService.search(searchQuery, params, response => {
if (response.error) {
model.clear()
totalResults = 0
isSearching = false
return
}
if (response.result) {
updateModel(response.result)
}
isSearching = false
searchCompleted()
})
}
function updateModel(result) {
model.clear()
totalResults = result.total_hits || 0
selectedIndex = 0
keyboardNavigationActive = true
if (!result.hits || result.hits.length === 0) {
selectedIndex = -1
keyboardNavigationActive = false
return
}
for (var i = 0; i < result.hits.length; i++) {
const hit = result.hits[i]
const filePath = hit.id || ""
const fileName = getFileName(filePath)
const fileExt = getFileExtension(fileName)
const fileType = determineFileType(fileName, filePath)
const dirPath = getDirPath(filePath)
model.append({
"filePath": filePath,
"fileName": fileName,
"fileExtension": fileExt,
"fileType": fileType,
"dirPath": dirPath,
"score": hit.score || 0
})
}
}
function getFileName(path) {
const parts = path.split('/')
return parts[parts.length - 1] || path
}
function getFileExtension(fileName) {
const parts = fileName.split('.')
if (parts.length > 1) {
return parts[parts.length - 1].toLowerCase()
}
return ""
}
function getDirPath(path) {
const lastSlash = path.lastIndexOf('/')
if (lastSlash > 0) {
return path.substring(0, lastSlash)
}
return ""
}
function determineFileType(fileName, filePath) {
const ext = getFileExtension(fileName)
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]
if (imageExts.includes(ext)) {
return "image"
}
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]
if (videoExts.includes(ext)) {
return "video"
}
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]
if (audioExts.includes(ext)) {
return "audio"
}
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]
if (codeExts.includes(ext)) {
return "code"
}
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]
if (docExts.includes(ext)) {
return "document"
}
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]
if (archiveExts.includes(ext)) {
return "archive"
}
if (!ext || fileName.indexOf('.') === -1) {
return "binary"
}
return "file"
}
function selectNext() {
if (model.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, model.count - 1)
}
function selectPrevious() {
if (model.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0)
}
signal fileOpened
function openFile(filePath) {
if (!filePath || filePath.length === 0) {
return
}
let url = filePath
if (!url.startsWith("file://")) {
url = "file://" + filePath
}
Qt.openUrlExternally(url)
fileOpened()
}
function openFolder(filePath) {
if (!filePath || filePath.length === 0) {
return
}
const lastSlash = filePath.lastIndexOf('/')
if (lastSlash <= 0) {
return
}
const dirPath = filePath.substring(0, lastSlash)
let url = dirPath
if (!url.startsWith("file://")) {
url = "file://" + dirPath
}
Qt.openUrlExternally(url)
fileOpened()
}
function openSelected() {
if (model.count === 0 || selectedIndex < 0 || selectedIndex >= model.count) {
return
}
const item = model.get(selectedIndex)
if (item && item.filePath) {
openFile(item.filePath)
}
}
function reset() {
searchQuery = ""
model.clear()
selectedIndex = -1
keyboardNavigationActive = false
isSearching = false
totalResults = 0
}
onSearchQueryChanged: {
performSearch()
}
onSearchFieldChanged: {
performSearch()
}
}

View File

@@ -0,0 +1,155 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: entry
required property string filePath
required property string fileName
required property string fileExtension
required property string fileType
required property string dirPath
required property bool isSelected
required property int itemIndex
signal clicked()
readonly property int iconSize: 40
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: iconSize
height: iconSize
anchors.verticalCenter: parent.verticalCenter
Image {
id: imagePreview
anchors.fill: parent
source: fileType === "image" ? `file://${filePath}` : ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
asynchronous: true
visible: fileType === "image" && status === Image.Ready
sourceSize.width: 128
sourceSize.height: 128
}
MultiEffect {
anchors.fill: parent
source: imagePreview
maskEnabled: true
maskSource: imageMask
visible: fileType === "image" && imagePreview.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: imageMask
width: iconSize
height: iconSize
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: getFileTypeColor()
visible: fileType !== "image" || imagePreview.status !== Image.Ready
StyledText {
anchors.centerIn: parent
text: getFileIconText()
font.pixelSize: fileExtension.length > 0 ? (fileExtension.length > 3 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall) : Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: fileName
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideMiddle
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
width: parent.width
text: dirPath
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 1
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: entry.clicked()
}
function getFileTypeColor() {
switch (fileType) {
case "code":
return Theme.codeFileColor || Theme.primarySelected
case "document":
return Theme.docFileColor || Theme.secondarySelected
case "video":
return Theme.videoFileColor || Theme.tertiarySelected
case "audio":
return Theme.audioFileColor || Theme.errorSelected
case "archive":
return Theme.archiveFileColor || Theme.warningSelected
case "binary":
return Theme.binaryFileColor || Theme.surfaceDim
default:
return Theme.surfaceLight
}
}
function getFileIconText() {
if (fileType === "binary") {
return "bin"
}
if (fileExtension.length > 0) {
return fileExtension
}
return fileName.charAt(0).toUpperCase()
}
}

View File

@@ -0,0 +1,246 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: resultsContainer
property var fileSearchController: null
function resetScroll() {
filesList.contentY = 0
}
color: "transparent"
clip: true
DankListView {
id: filesList
property int itemHeight: 60
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: fileSearchController ? fileSearchController.keyboardNavigationActive : false
signal keyboardNavigationReset
signal itemClicked(int index)
signal itemRightClicked(int index)
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
model: fileSearchController ? fileSearchController.model : null
currentIndex: fileSearchController ? fileSearchController.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: function (index) {
if (fileSearchController) {
const item = fileSearchController.model.get(index)
fileSearchController.openFile(item.filePath)
}
}
onItemRightClicked: function (index) {
if (fileSearchController) {
const item = fileSearchController.model.get(index)
fileSearchController.openFolder(item.filePath)
}
}
onKeyboardNavigationReset: {
if (fileSearchController)
fileSearchController.keyboardNavigationActive = false
}
delegate: Rectangle {
required property int index
required property string filePath
required property string fileName
required property string fileExtension
required property string fileType
required property string dirPath
width: ListView.view.width
height: filesList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : fileMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: 40
height: 40
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: iconBackground
anchors.fill: parent
radius: width / 2
color: Theme.surfaceLight
visible: fileType !== "image"
DankNFIcon {
id: nerdIcon
anchors.centerIn: parent
name: {
const lowerName = fileName.toLowerCase()
if (lowerName.startsWith("dockerfile"))
return "docker"
if (lowerName.startsWith("makefile"))
return "makefile"
if (lowerName.startsWith("license"))
return "license"
if (lowerName.startsWith("readme"))
return "readme"
return fileExtension.toLowerCase()
}
size: Theme.fontSizeXLarge
color: Theme.surfaceText
}
StyledText {
anchors.centerIn: parent
text: fileExtension ? (fileExtension.length > 4 ? fileExtension.substring(0, 4) : fileExtension) : "?"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Bold
visible: !nerdIcon.visible
}
}
Loader {
anchors.fill: parent
active: fileType === "image"
sourceComponent: Image {
anchors.fill: parent
source: "file://" + filePath
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: 40
height: 40
radius: 20
}
}
}
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 40 - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: fileName || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideMiddle
maximumLineCount: 1
}
StyledText {
width: parent.width
text: dirPath || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 1
}
}
}
MouseArea {
id: fileMouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
z: 10
onEntered: {
if (filesList.hoverUpdatesSelection && !filesList.keyboardNavigationActive)
filesList.currentIndex = index
}
onPositionChanged: {
filesList.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
filesList.itemClicked(index)
} else if (mouse.button === Qt.RightButton) {
filesList.itemRightClicked(index)
}
}
}
}
}
Item {
anchors.fill: parent
visible: !fileSearchController || !fileSearchController.model || fileSearchController.model.count === 0
StyledText {
property string displayText: {
if (!fileSearchController) {
return ""
}
if (!DSearchService.dsearchAvailable) {
return I18n.tr("DankSearch not available")
}
if (fileSearchController.isSearching) {
return I18n.tr("Searching...")
}
if (fileSearchController.searchQuery.length === 0) {
return I18n.tr("Enter a search query")
}
if (!fileSearchController.model || fileSearchController.model.count === 0) {
return I18n.tr("No files found")
}
return ""
}
text: displayText
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: displayText.length > 0
}
}
}

View File

@@ -11,10 +11,40 @@ Item {
property alias appLauncher: appLauncher property alias appLauncher: appLauncher
property alias searchField: searchField property alias searchField: searchField
property alias fileSearchController: fileSearchController
property var parentModal: null property var parentModal: null
property string searchMode: "apps"
function resetScroll() { function resetScroll() {
if (searchMode === "apps") {
resultsView.resetScroll() resultsView.resetScroll()
} else {
fileSearchResults.resetScroll()
}
}
function updateSearchMode() {
if (searchField.text.startsWith("/")) {
if (searchMode !== "files") {
searchMode = "files"
}
const query = searchField.text.substring(1)
fileSearchController.searchQuery = query
} else {
if (searchMode !== "apps") {
searchMode = "apps"
fileSearchController.reset()
appLauncher.searchQuery = searchField.text
}
}
}
onSearchModeChanged: {
if (searchMode === "files") {
appLauncher.keyboardNavigationActive = false
} else {
fileSearchController.keyboardNavigationActive = false
}
} }
anchors.fill: parent anchors.fill: parent
@@ -27,59 +57,95 @@ Item {
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Down) { } else if (event.key === Qt.Key_Down) {
if (searchMode === "apps") {
appLauncher.selectNext() appLauncher.selectNext()
} else {
fileSearchController.selectNext()
}
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Up) { } else if (event.key === Qt.Key_Up) {
if (searchMode === "apps") {
appLauncher.selectPrevious() appLauncher.selectPrevious()
} else {
fileSearchController.selectPrevious()
}
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") { } else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow() appLauncher.selectNextInRow()
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") { } else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow() appLauncher.selectPreviousInRow()
event.accepted = true event.accepted = true
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) { } else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
appLauncher.selectNext() appLauncher.selectNext()
} else {
fileSearchController.selectNext()
}
event.accepted = true event.accepted = true
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) { } else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
appLauncher.selectPrevious() appLauncher.selectPrevious()
} else {
fileSearchController.selectPrevious()
}
event.accepted = true event.accepted = true
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") { } else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow() appLauncher.selectNextInRow()
event.accepted = true event.accepted = true
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") { } else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow() appLauncher.selectPreviousInRow()
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Tab) { } else if (event.key === Qt.Key_Tab) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") { if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow() appLauncher.selectNextInRow()
} else { } else {
appLauncher.selectNext() appLauncher.selectNext()
} }
} else {
fileSearchController.selectNext()
}
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Backtab) { } else if (event.key === Qt.Key_Backtab) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") { if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow() appLauncher.selectPreviousInRow()
} else { } else {
appLauncher.selectPrevious() appLauncher.selectPrevious()
} }
} else {
fileSearchController.selectPrevious()
}
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) { } else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") { if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow() appLauncher.selectNextInRow()
} else { } else {
appLauncher.selectNext() appLauncher.selectNext()
} }
} else {
fileSearchController.selectNext()
}
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) { } else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") { if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow() appLauncher.selectPreviousInRow()
} else { } else {
appLauncher.selectPrevious() appLauncher.selectPrevious()
} }
} else {
fileSearchController.selectPrevious()
}
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (searchMode === "apps") {
appLauncher.launchSelected() appLauncher.launchSelected()
} else if (searchMode === "files") {
fileSearchController.openSelected()
}
event.accepted = true event.accepted = true
} }
} }
@@ -98,6 +164,15 @@ Item {
} }
} }
FileSearchController {
id: fileSearchController
onFileOpened: () => {
if (parentModal)
parentModal.hide()
}
}
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
@@ -118,7 +193,7 @@ Item {
backgroundColor: Theme.surfaceContainerHigh backgroundColor: Theme.surfaceContainerHigh
normalBorderColor: Theme.outlineMedium normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary focusedBorderColor: Theme.primary
leftIconName: "search" leftIconName: searchMode === "files" ? "folder" : "search"
leftIconSize: Theme.iconSize leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary leftIconFocusedColor: Theme.primary
@@ -126,14 +201,18 @@ Item {
textColor: Theme.surfaceText textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
enabled: parentModal ? parentModal.spotlightOpen : true enabled: parentModal ? parentModal.spotlightOpen : true
placeholderText: "" placeholderText: searchMode === "files" ? "Search files..." : "Search apps..."
ignoreLeftRightKeys: appLauncher.viewMode !== "list" ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true ignoreTabKeys: true
keyForwardTargets: [spotlightKeyHandler] keyForwardTargets: [spotlightKeyHandler]
text: appLauncher.searchQuery onTextChanged: {
onTextEdited: () => { if (searchMode === "apps") {
appLauncher.searchQuery = text appLauncher.searchQuery = text
} }
}
onTextEdited: {
updateSearchMode()
}
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (parentModal) if (parentModal)
@@ -141,12 +220,18 @@ Item {
event.accepted = true event.accepted = true
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) { } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (searchMode === "apps") {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected() appLauncher.launchSelected()
else if (appLauncher.model.count > 0) else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0)) appLauncher.launchApp(appLauncher.model.get(0))
} else if (searchMode === "files") {
if (fileSearchController.model.count > 0)
fileSearchController.openSelected()
}
event.accepted = true 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_Tab || event.key === Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) { } 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_Tab || event.key
=== Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false event.accepted = false
} }
} }
@@ -154,7 +239,7 @@ Item {
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: appLauncher.model.count > 0 visible: searchMode === "apps" && appLauncher.model.count > 0
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Rectangle { Rectangle {
@@ -207,12 +292,116 @@ Item {
} }
} }
} }
Row {
spacing: Theme.spacingXS
visible: searchMode === "files"
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: filenameFilterButton
width: 36
height: 36
radius: Theme.cornerRadius
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "title"
size: 18
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
} }
MouseArea {
id: filenameFilterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
fileSearchController.searchField = "filename"
}
onEntered: {
filenameTooltipLoader.active = true
Qt.callLater(() => {
if (filenameTooltipLoader.item) {
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null)
}
})
}
onExited: {
if (filenameTooltipLoader.item)
filenameTooltipLoader.item.hide()
filenameTooltipLoader.active = false
}
}
}
Rectangle {
id: contentFilterButton
width: 36
height: 36
radius: Theme.cornerRadius
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "description"
size: 18
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: contentFilterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
fileSearchController.searchField = "body"
}
onEntered: {
contentTooltipLoader.active = true
Qt.callLater(() => {
if (contentTooltipLoader.item) {
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null)
}
})
}
onExited: {
if (contentTooltipLoader.item)
contentTooltipLoader.item.hide()
contentTooltipLoader.active = false
}
}
}
}
}
Item {
width: parent.width
height: parent.height - y
SpotlightResults { SpotlightResults {
id: resultsView id: resultsView
anchors.fill: parent
appLauncher: spotlightKeyHandler.appLauncher appLauncher: spotlightKeyHandler.appLauncher
contextMenu: contextMenu contextMenu: contextMenu
visible: searchMode === "apps"
}
FileSearchResults {
id: fileSearchResults
anchors.fill: parent
fileSearchController: spotlightKeyHandler.fileSearchController
visible: searchMode === "files"
}
} }
} }
@@ -233,7 +422,6 @@ Item {
MouseArea { MouseArea {
// Prevent closing when clicking on the menu itself
x: contextMenu.x x: contextMenu.x
y: contextMenu.y y: contextMenu.y
width: contextMenu.width width: contextMenu.width
@@ -241,4 +429,18 @@ Item {
onClicked: () => {} onClicked: () => {}
} }
} }
Loader {
id: filenameTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Loader {
id: contentTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
} }

View File

@@ -58,6 +58,9 @@ DankModal {
spotlightContent.appLauncher.selectedIndex = 0 spotlightContent.appLauncher.selectedIndex = 0
spotlightContent.appLauncher.setCategory(I18n.tr("All")) spotlightContent.appLauncher.setCategory(I18n.tr("All"))
} }
if (spotlightContent.fileSearchController) {
spotlightContent.fileSearchController.reset()
}
if (spotlightContent.resetScroll) { if (spotlightContent.resetScroll) {
spotlightContent.resetScroll() spotlightContent.resetScroll()
} }

View File

@@ -7,10 +7,6 @@ import qs.Widgets
Rectangle { Rectangle {
id: resultsContainer id: resultsContainer
// DEVELOPER NOTE: This component renders the Spotlight launcher (accessed via Mod+Space).
// Changes to launcher behavior, especially item rendering, filtering, or model structure,
// likely require corresponding updates in Modules/AppDrawer/AppLauncher.qml and vice versa.
property var appLauncher: null property var appLauncher: null
property var contextMenu: null property var contextMenu: null
@@ -19,8 +15,6 @@ Rectangle {
resultsGrid.contentY = 0 resultsGrid.contentY = 0
} }
width: parent.width
height: parent.height - y
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: "transparent" color: "transparent"
clip: true clip: true

View File

@@ -54,17 +54,25 @@ Variants {
} }
function getFillMode(modeName) { function getFillMode(modeName) {
switch(modeName) { switch (modeName) {
case "Stretch": return Image.Stretch case "Stretch":
return Image.Stretch
case "Fit": case "Fit":
case "PreserveAspectFit": return Image.PreserveAspectFit case "PreserveAspectFit":
return Image.PreserveAspectFit
case "Fill": case "Fill":
case "PreserveAspectCrop": return Image.PreserveAspectCrop case "PreserveAspectCrop":
case "Tile": return Image.Tile return Image.PreserveAspectCrop
case "TileVertically": return Image.TileVertically case "Tile":
case "TileHorizontally": return Image.TileHorizontally return Image.Tile
case "Pad": return Image.Pad case "TileVertically":
default: return Image.PreserveAspectCrop return Image.TileVertically
case "TileHorizontally":
return Image.TileHorizontally
case "Pad":
return Image.Pad
default:
return Image.PreserveAspectCrop
} }
} }
@@ -76,32 +84,76 @@ Variants {
Component.onCompleted: { Component.onCompleted: {
if (source) { if (source) {
const formattedSource = source.startsWith("file://") ? source : "file://" + source const formattedSource = source.startsWith("file://") ? source : "file://" + source
wallpaperImage.source = formattedSource setWallpaperImmediate(formattedSource)
} }
isInitialized = true
} }
Component.onDestruction: { Component.onDestruction: {
weProc.stop() weProc.stop()
} }
property bool isInitialized: false
property real transitionProgress: 0
readonly property bool transitioning: transitionAnimation.running
onSourceChanged: { onSourceChanged: {
const isWE = source.startsWith("we:") const isWE = source.startsWith("we:")
const isColor = source.startsWith("#") const isColor = source.startsWith("#")
if (isWE) { if (isWE) {
wallpaperImage.source = "" setWallpaperImmediate("")
weProc.start(source.substring(3)) weProc.start(source.substring(3))
} else { } else {
weProc.stop() weProc.stop()
if (!source) { if (!source) {
wallpaperImage.source = "" setWallpaperImmediate("")
} else if (isColor) { } else if (isColor) {
wallpaperImage.source = "" setWallpaperImmediate("")
} else { } else {
wallpaperImage.source = source.startsWith("file://") ? source : "file://" + source if (!isInitialized || !currentWallpaper.source) {
setWallpaperImmediate(source.startsWith("file://") ? source : "file://" + source)
isInitialized = true
} else {
changeWallpaper(source.startsWith("file://") ? source : "file://" + source)
} }
} }
} }
}
function setWallpaperImmediate(newSource) {
transitionAnimation.stop()
root.transitionProgress = 0.0
currentWallpaper.source = newSource
nextWallpaper.source = ""
currentWallpaper.opacity = 1
nextWallpaper.opacity = 0
}
function changeWallpaper(newPath) {
if (newPath === currentWallpaper.source)
return
if (!newPath || newPath.startsWith("#"))
return
if (root.transitioning) {
transitionAnimation.stop()
root.transitionProgress = 0
currentWallpaper.source = nextWallpaper.source
nextWallpaper.source = ""
}
if (!currentWallpaper.source) {
setWallpaperImmediate(newPath)
return
}
nextWallpaper.source = newPath
if (nextWallpaper.status === Image.Ready) {
transitionAnimation.start()
}
}
Loader { Loader {
anchors.fill: parent anchors.fill: parent
@@ -114,21 +166,78 @@ Variants {
} }
Image { Image {
id: wallpaperImage id: currentWallpaper
anchors.fill: parent anchors.fill: parent
visible: false visible: false
opacity: 1
asynchronous: true asynchronous: true
smooth: true smooth: true
cache: true cache: true
fillMode: root.getFillMode(SettingsData.wallpaperFillMode) fillMode: root.getFillMode(SettingsData.wallpaperFillMode)
} }
Image {
id: nextWallpaper
anchors.fill: parent
visible: false
opacity: 0
asynchronous: true
smooth: true
cache: true
fillMode: root.getFillMode(SettingsData.wallpaperFillMode)
onStatusChanged: {
if (status !== Image.Ready)
return
if (!root.transitioning) {
transitionAnimation.start()
}
}
}
Item {
id: blurredLayer
anchors.fill: parent
MultiEffect { MultiEffect {
anchors.fill: parent anchors.fill: parent
source: wallpaperImage source: currentWallpaper
blurEnabled: true blurEnabled: true
blur: 0.8 blur: 0.8
blurMax: 48 blurMax: 75
opacity: 1 - root.transitionProgress
}
MultiEffect {
anchors.fill: parent
source: nextWallpaper
blurEnabled: true
blur: 0.8
blurMax: 75
opacity: root.transitionProgress
}
}
NumberAnimation {
id: transitionAnimation
target: root
property: "transitionProgress"
from: 0.0
to: 1.0
duration: 1000
easing.type: Easing.InOutCubic
onFinished: {
Qt.callLater(() => {
if (nextWallpaper.source && nextWallpaper.status === Image.Ready && !nextWallpaper.source.toString().startsWith("#")) {
currentWallpaper.source = nextWallpaper.source
}
nextWallpaper.source = ""
currentWallpaper.opacity = 1
nextWallpaper.opacity = 0
root.transitionProgress = 0.0
})
}
} }
} }
} }

View File

@@ -13,6 +13,7 @@ PluginComponent {
service: DMSNetworkService service: DMSNetworkService
} }
ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off") ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
ccWidgetPrimaryText: "VPN" ccWidgetPrimaryText: "VPN"
ccWidgetSecondaryText: { ccWidgetSecondaryText: {
@@ -26,11 +27,7 @@ PluginComponent {
ccWidgetIsActive: DMSNetworkService.connected ccWidgetIsActive: DMSNetworkService.connected
onCcWidgetToggled: { onCcWidgetToggled: {
if (DMSNetworkService.connected) { DMSNetworkService.toggleVpn()
DMSNetworkService.disconnectAllActive()
} else if (DMSNetworkService.profiles.length > 0) {
DMSNetworkService.connect(DMSNetworkService.profiles[0].uuid)
}
} }
ccDetailContent: Component { ccDetailContent: Component {
@@ -62,10 +59,10 @@ PluginComponent {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
} elide: Text.ElideRight
wrapMode: Text.NoWrap
Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: parent.width - 120
} }
Rectangle { Rectangle {
@@ -75,6 +72,7 @@ PluginComponent {
visible: DMSNetworkService.connected visible: DMSNetworkService.connected
width: 110 width: 110
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
@@ -98,7 +96,8 @@ PluginComponent {
id: discAllArea id: discAllArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.disconnectAllActive() onClicked: DMSNetworkService.disconnectAllActive()
} }
} }
@@ -165,6 +164,7 @@ PluginComponent {
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight) color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1 border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
RowLayout { RowLayout {
anchors.left: parent.left anchors.left: parent.left
@@ -183,11 +183,15 @@ PluginComponent {
Column { Column {
spacing: 2 spacing: 2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText { StyledText {
text: modelData.name text: modelData.name
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
} }
StyledText { StyledText {
@@ -233,7 +237,8 @@ PluginComponent {
id: rowArea id: rowArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(modelData.uuid) onClicked: DMSNetworkService.toggle(modelData.uuid)
} }
} }

View File

@@ -219,9 +219,13 @@ Column {
{ {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return "sync" return "sync"
if (NetworkService.networkStatus === "ethernet")
const status = NetworkService.networkStatus
if (status === "ethernet")
return "settings_ethernet" return "settings_ethernet"
if (NetworkService.networkStatus === "wifi") if (status === "vpn")
return NetworkService.ethernetConnected ? "settings_ethernet" : NetworkService.wifiSignalIcon
if (status === "wifi")
return NetworkService.wifiSignalIcon return NetworkService.wifiSignalIcon
if (NetworkService.wifiEnabled) if (NetworkService.wifiEnabled)
return "wifi_off" return "wifi_off"
@@ -266,9 +270,17 @@ Column {
{ {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..." return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
if (NetworkService.networkStatus === "ethernet")
const status = NetworkService.networkStatus
if (status === "ethernet")
return "Ethernet" return "Ethernet"
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) if (status === "vpn") {
if (NetworkService.ethernetConnected)
return "Ethernet"
if (NetworkService.wifiConnected && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID
}
if (status === "wifi" && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID return NetworkService.currentWifiSSID
if (NetworkService.wifiEnabled) if (NetworkService.wifiEnabled)
return "Not connected" return "Not connected"
@@ -298,9 +310,17 @@ Column {
{ {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return "Please wait..." return "Please wait..."
if (NetworkService.networkStatus === "ethernet")
const status = NetworkService.networkStatus
if (status === "ethernet")
return "Connected" return "Connected"
if (NetworkService.networkStatus === "wifi") if (status === "vpn") {
if (NetworkService.ethernetConnected)
return "Connected"
if (NetworkService.wifiConnected)
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
}
if (status === "wifi")
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected" return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
if (NetworkService.wifiEnabled) if (NetworkService.wifiEnabled)
return "Select network" return "Select network"
@@ -358,9 +378,13 @@ Column {
{ {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return false return false
if (NetworkService.networkStatus === "ethernet")
const status = NetworkService.networkStatus
if (status === "ethernet")
return true return true
if (NetworkService.networkStatus === "wifi") if (status === "vpn")
return NetworkService.ethernetConnected || NetworkService.wifiConnected
if (status === "wifi")
return true return true
return NetworkService.wifiEnabled return NetworkService.wifiEnabled
} }

View File

@@ -18,17 +18,7 @@ Item {
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0) anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0) anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
readonly property real dpr: { readonly property real dpr: CompositorService.getScreenScale(barWindow.screen)
if (CompositorService.isNiri && barWindow.screen) {
const niriScale = NiriService.displayScales[barWindow.screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && barWindow.screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === barWindow.screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return barWindow.screen?.devicePixelRatio || 1
}
function requestRepaint() { function requestRepaint() {
debounceTimer.restart() debounceTimer.restart()

View File

@@ -372,9 +372,6 @@ Item {
} }
item.widthChanged.connect(() => layoutTimer.restart()) item.widthChanged.connect(() => layoutTimer.restart())
item.heightChanged.connect(() => layoutTimer.restart()) item.heightChanged.connect(() => layoutTimer.restart())
if (model.widgetId === "spacer") {
item.spacerSize = Qt.binding(() => model.size || 20)
}
if (root.axis && "axis" in item) { if (root.axis && "axis" in item) {
item.axis = Qt.binding(() => root.axis) item.axis = Qt.binding(() => root.axis)
} }

View File

@@ -4,6 +4,7 @@ import QtQuick.Effects
import QtQuick.Shapes import QtQuick.Shapes
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.I3
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
@@ -31,6 +32,9 @@ Item {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name focusedScreenName = Hyprland.focusedWorkspace.monitor.name
} else if (CompositorService.isNiri && NiriService.currentOutput) { } else if (CompositorService.isNiri && NiriService.currentOutput) {
focusedScreenName = NiriService.currentOutput focusedScreenName = NiriService.currentOutput
} else if (CompositorService.isSway) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
focusedScreenName = focusedWs?.monitor?.name || ""
} }
if (!focusedScreenName && barVariants.instances.length > 0) { if (!focusedScreenName && barVariants.instances.length > 0) {
@@ -55,6 +59,9 @@ Item {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name focusedScreenName = Hyprland.focusedWorkspace.monitor.name
} else if (CompositorService.isNiri && NiriService.currentOutput) { } else if (CompositorService.isNiri && NiriService.currentOutput) {
focusedScreenName = NiriService.currentOutput focusedScreenName = NiriService.currentOutput
} else if (CompositorService.isSway) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
focusedScreenName = focusedWs?.monitor?.name || ""
} }
if (!focusedScreenName && barVariants.instances.length > 0) { if (!focusedScreenName && barVariants.instances.length > 0) {
@@ -110,9 +117,9 @@ Item {
return return
} }
if (clockButtonRef && dankDashPopoutLoader.item.setTriggerPosition) { if (clockButtonRef && clockButtonRef.visualContent && dankDashPopoutLoader.item.setTriggerPosition) {
const globalPos = clockButtonRef.mapToGlobal(0, 0) const globalPos = clockButtonRef.visualContent.mapToGlobal(0, 0)
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, clockButtonRef.width) const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, clockButtonRef.visualWidth)
const section = clockButtonRef.section || "center" const section = clockButtonRef.section || "center"
dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen) dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen)
} else { } else {
@@ -173,19 +180,7 @@ Item {
readonly property color _surfaceContainer: Theme.surfaceContainer readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property real _backgroundAlpha: topBarCore?.backgroundTransparency ?? SettingsData.dankBarTransparency readonly property real _backgroundAlpha: topBarCore?.backgroundTransparency ?? SettingsData.dankBarTransparency
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha) readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
readonly property real _dpr: { readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen)
if (CompositorService.isNiri && barWindow.screen) {
const niriScale = NiriService.displayScales[barWindow.screen.name]
if (niriScale !== undefined)
return niriScale
}
if (CompositorService.isHyprland && barWindow.screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === barWindow.screen.name)
if (hyprlandMonitor?.scale !== undefined)
return hyprlandMonitor.scale
}
return (barWindow.screen?.devicePixelRatio) || 1
}
property string screenName: modelData.name property string screenName: modelData.name
readonly property int notificationCount: NotificationService.notifications.length readonly property int notificationCount: NotificationService.notifications.length
@@ -200,11 +195,6 @@ Item {
property var nativeInhibitor: null property var nativeInhibitor: null
Component.onCompleted: { Component.onCompleted: {
const fonts = Qt.fontFamilies()
if (fonts.indexOf("Material Symbols Rounded") === -1) {
ToastService.showError("Please install Material Symbols Rounded and Restart your Shell. See README.md for instructions")
}
if (SettingsData.forceStatusBarLayoutRefresh) { if (SettingsData.forceStatusBarLayoutRefresh) {
SettingsData.forceStatusBarLayoutRefresh.connect(() => { SettingsData.forceStatusBarLayoutRefresh.connect(() => {
Qt.callLater(() => { Qt.callLater(() => {
@@ -556,6 +546,163 @@ Item {
componentMapRevision++ componentMapRevision++
} }
readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, barWindow.screenName);
}
function getRealWorkspaces() {
if (CompositorService.isNiri) {
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) {
return NiriService.getCurrentOutputWorkspaceNumbers()
}
const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName).map(ws => ws.idx + 1)
return workspaces.length > 0 ? workspaces : [1, 2]
} else if (CompositorService.isHyprland) {
const workspaces = Hyprland.workspaces?.values || []
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) {
const sorted = workspaces.slice().sort((a, b) => a.id - b.id)
const filtered = sorted.filter(ws => ws.id > -1)
return filtered.length > 0 ? filtered : [{"id": 1, "name": "1"}]
}
const monitorWorkspaces = workspaces.filter(ws => {
return ws.lastIpcObject && ws.lastIpcObject.monitor === barWindow.screenName && ws.id > -1
})
if (monitorWorkspaces.length === 0) {
return [{"id": 1, "name": "1"}]
}
return monitorWorkspaces.sort((a, b) => a.id - b.id)
} else if (CompositorService.isDwl) {
if (!DwlService.dwlAvailable) {
return [0]
}
if (SettingsData.dwlShowAllTags) {
return Array.from({length: DwlService.tagCount}, (_, i) => i)
}
return DwlService.getVisibleTags(barWindow.screenName)
} else if (CompositorService.isSway) {
const workspaces = I3.workspaces?.values || []
if (workspaces.length === 0) return [{"num": 1}]
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) {
return workspaces.slice().sort((a, b) => a.num - b.num)
}
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === barWindow.screenName)
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}]
}
return [1]
}
function getCurrentWorkspace() {
if (CompositorService.isNiri) {
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) {
return NiriService.getCurrentWorkspaceNumber()
}
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active)
return activeWs ? activeWs.idx + 1 : 1
} else if (CompositorService.isHyprland) {
const monitors = Hyprland.monitors?.values || []
const currentMonitor = monitors.find(monitor => monitor.name === barWindow.screenName)
return currentMonitor?.activeWorkspace?.id ?? 1
} else if (CompositorService.isDwl) {
if (!DwlService.dwlAvailable) return 0
const outputState = DwlService.getOutputState(barWindow.screenName)
if (!outputState || !outputState.tags) return 0
const activeTags = DwlService.getActiveTags(barWindow.screenName)
return activeTags.length > 0 ? activeTags[0] : 0
} else if (CompositorService.isSway) {
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
return focusedWs ? focusedWs.num : 1
}
const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === barWindow.screenName && ws.focused === true)
return focusedWs ? focusedWs.num : 1
}
return 1
}
function switchWorkspace(direction) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
if (CompositorService.isNiri) {
const currentWs = getCurrentWorkspace()
const currentIndex = realWorkspaces.findIndex(ws => ws === currentWs)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex !== validIndex) {
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1)
}
} else if (CompositorService.isHyprland) {
const currentWs = getCurrentWorkspace()
const currentIndex = realWorkspaces.findIndex(ws => ws.id === currentWs)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex !== validIndex) {
Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`)
}
} else if (CompositorService.isDwl) {
const currentTag = getCurrentWorkspace()
const currentIndex = realWorkspaces.findIndex(tag => tag === currentTag)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex !== validIndex) {
DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex])
}
} else if (CompositorService.isSway) {
const currentWs = getCurrentWorkspace()
const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex !== validIndex) {
try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){}
}
}
}
function switchApp(deltaY) {
const windows = sortedToplevels;
if (windows.length < 2) {
return;
}
let currentIndex = -1;
for (let i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i;
break;
}
}
let nextIndex;
if (deltaY < 0) {
if (currentIndex === -1) {
nextIndex = 0;
} else {
nextIndex = currentIndex + 1;
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length - 1;
} else {
nextIndex = currentIndex - 1;
}
}
const nextWindow = windows[nextIndex];
if (nextWindow) {
nextWindow.activate();
}
}
readonly property int availableWidth: width readonly property int availableWidth: width
readonly property int launcherButtonWidth: 40 readonly property int launcherButtonWidth: 40
readonly property int workspaceSwitcherWidth: 120 readonly property int workspaceSwitcherWidth: 120
@@ -688,6 +835,60 @@ Item {
id: stackContainer id: stackContainer
anchors.fill: parent anchors.fill: parent
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
z: -1
property real scrollAccumulator: 0
property real touchpadThreshold: 500
property bool actionInProgress: false
Timer {
id: cooldownTimer
interval: 100
onTriggered: parent.actionInProgress = false
}
onWheel: wheel => {
if (actionInProgress) {
wheel.accepted = false
return
}
const deltaY = wheel.angleDelta.y
const deltaX = wheel.angleDelta.x
if (CompositorService.isNiri && Math.abs(deltaX) > Math.abs(deltaY)) {
topBarContent.switchApp(deltaX)
wheel.accepted = false
return
}
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
const direction = deltaY < 0 ? 1 : -1
if (isMouseWheel) {
topBarContent.switchWorkspace(direction)
actionInProgress = true
cooldownTimer.restart()
} else {
scrollAccumulator += deltaY
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
const touchDirection = scrollAccumulator < 0 ? 1 : -1
topBarContent.switchWorkspace(touchDirection)
scrollAccumulator = 0
actionInProgress = true
cooldownTimer.restart()
}
}
wheel.accepted = false
}
}
Item { Item {
id: horizontalStack id: horizontalStack
anchors.fill: parent anchors.fill: parent

View File

@@ -17,6 +17,15 @@ DankPopout {
service: DMSNetworkService service: DMSNetworkService
} }
property bool wasVisible: false
onShouldBeVisibleChanged: {
if (shouldBeVisible && !wasVisible) {
DMSNetworkService.getState()
}
wasVisible = shouldBeVisible
}
property var triggerScreen: null property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) { function setTriggerPosition(x, y, width, section, screen) {
@@ -175,11 +184,10 @@ DankPopout {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
} elide: Text.ElideRight
wrapMode: Text.NoWrap
Item {
Layout.fillWidth: true Layout.fillWidth: true
height: 1 Layout.maximumWidth: parent.width - 140
} }
// Removed Quick Connect for clarity // Removed Quick Connect for clarity
@@ -198,6 +206,7 @@ DankPopout {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
border.width: 0 border.width: 0
border.color: Theme.outlineLight border.color: Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
@@ -223,7 +232,8 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.disconnectAllActive() onClicked: DMSNetworkService.disconnectAllActive()
} }
@@ -295,6 +305,7 @@ DankPopout {
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight) color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1 border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
RowLayout { RowLayout {
anchors.left: parent.left anchors.left: parent.left
@@ -313,11 +324,15 @@ DankPopout {
Column { Column {
spacing: 2 spacing: 2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText { StyledText {
text: modelData.name text: modelData.name
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
} }
StyledText { StyledText {
@@ -391,7 +406,8 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(modelData.uuid) onClicked: DMSNetworkService.toggle(modelData.uuid)
} }

View File

@@ -31,10 +31,15 @@ BasePill {
return "sync" return "sync"
} }
if (NetworkService.networkStatus === "ethernet") { const status = NetworkService.networkStatus
if (status === "ethernet") {
return "lan" return "lan"
} }
if (status === "vpn") {
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
}
return NetworkService.wifiSignalIcon return NetworkService.wifiSignalIcon
} }
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
@@ -125,22 +130,27 @@ BasePill {
name: { name: {
if (NetworkService.wifiToggling) { if (NetworkService.wifiToggling) {
return "sync"; return "sync"
} }
if (NetworkService.networkStatus === "ethernet") { const status = NetworkService.networkStatus
return "lan"; if (status === "ethernet") {
return "lan"
} }
return NetworkService.wifiSignalIcon; if (status === "vpn") {
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
}
return NetworkService.wifiSignalIcon
} }
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: { color: {
if (NetworkService.wifiToggling) { if (NetworkService.wifiToggling) {
return Theme.primary; return Theme.primary
} }
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton; return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable visible: root.showNetworkIcon && NetworkService.networkAvailable

View File

@@ -11,7 +11,8 @@ import qs.Widgets
BasePill { BasePill {
id: root id: root
property string currentLayout: "" property bool compactMode: SettingsData.keyboardLayoutNameCompactMode
property string currentLayout: CompositorService.isNiri ? NiriService.getCurrentKeyboardLayoutName() : ""
property string hyprlandKeyboard: "" property string hyprlandKeyboard: ""
content: Component { content: Component {
@@ -82,24 +83,14 @@ BasePill {
} }
} }
Timer {
id: updateTimer
interval: 1000
running: true
repeat: true
onTriggered: {
updateLayout()
}
}
Component.onCompleted: { Component.onCompleted: {
if (CompositorService.isHyprland) {
updateLayout() updateLayout()
} }
}
function updateLayout() { function updateLayout() {
if (CompositorService.isNiri) { if (CompositorService.isHyprland) {
root.currentLayout = NiriService.getCurrentKeyboardLayoutName()
} else if (CompositorService.isHyprland) {
Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => { Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => {
if (exitCode !== 0) { if (exitCode !== 0) {
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
@@ -109,11 +100,34 @@ BasePill {
const data = JSON.parse(output) const data = JSON.parse(output)
const mainKeyboard = data.keyboards.find(kb => kb.main === true) const mainKeyboard = data.keyboards.find(kb => kb.main === true)
root.hyprlandKeyboard = mainKeyboard.name root.hyprlandKeyboard = mainKeyboard.name
if (mainKeyboard && mainKeyboard.active_keymap) {
if (mainKeyboard) {
const layout = mainKeyboard.layout
const variant = mainKeyboard.variant
const index = mainKeyboard.active_layout_index
if (root.compactMode && layout && variant && index !== undefined) {
const layouts = mainKeyboard.layout.split(",")
const variants = mainKeyboard.variant.split(",")
const index = mainKeyboard.active_layout_index
if (layouts[index] && variants[index] !== undefined) {
if (variants[index] === "") {
root.currentLayout = layouts[index]
} else {
root.currentLayout = layouts[index] + "-" + variants[index]
}
} else {
root.currentLayout = "Unknown"
}
} else if (mainKeyboard && mainKeyboard.active_keymap) {
root.currentLayout = mainKeyboard.active_keymap root.currentLayout = mainKeyboard.active_keymap
} else { } else {
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} }
} else {
root.currentLayout = "Unknown"
}
} catch (e) { } catch (e) {
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} }

View File

@@ -37,7 +37,7 @@ BasePill {
} }
IconImage { IconImage {
visible: SettingsData.launcherLogoMode === "compositor" visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway)
anchors.centerIn: parent anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset) width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset) height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
@@ -48,6 +48,10 @@ BasePill {
return "file://" + Theme.shellDir + "/assets/niri.svg" return "file://" + Theme.shellDir + "/assets/niri.svg"
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
return "file://" + Theme.shellDir + "/assets/hyprland.svg" return "file://" + Theme.shellDir + "/assets/hyprland.svg"
} else if (CompositorService.isDwl) {
return "file://" + Theme.shellDir + "/assets/mango.png"
} else if (CompositorService.isSway) {
return "file://" + Theme.shellDir + "/assets/sway.svg"
} }
return "" return ""
} }

View File

@@ -289,10 +289,10 @@ Item {
IconImage { IconImage {
id: iconImg id: iconImg
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - 18) / 2 : Theme.spacingXS anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 18 width: Theme.barIconSize(root.barThickness)
height: 18 height: Theme.barIconSize(root.barThickness)
source: { source: {
root._desktopEntriesUpdateTrigger root._desktopEntriesUpdateTrigger
const moddedId = Paths.moddedAppId(appId) const moddedId = Paths.moddedAppId(appId)
@@ -309,9 +309,9 @@ Item {
DankIcon { DankIcon {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - 18) / 2 : Theme.spacingXS anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
size: 18 size: Theme.barIconSize(root.barThickness)
name: "sports_esports" name: "sports_esports"
color: Theme.surfaceText color: Theme.surfaceText
visible: { visible: {
@@ -517,10 +517,10 @@ Item {
IconImage { IconImage {
id: iconImg id: iconImg
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - 18) / 2 : Theme.spacingXS anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 18 width: Theme.barIconSize(root.barThickness)
height: 18 height: Theme.barIconSize(root.barThickness)
source: { source: {
root._desktopEntriesUpdateTrigger root._desktopEntriesUpdateTrigger
const moddedId = Paths.moddedAppId(appId) const moddedId = Paths.moddedAppId(appId)
@@ -537,9 +537,9 @@ Item {
DankIcon { DankIcon {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - 18) / 2 : Theme.spacingXS anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
size: 18 size: Theme.barIconSize(root.barThickness)
name: "sports_esports" name: "sports_esports"
color: Theme.surfaceText color: Theme.surfaceText
visible: { visible: {

View File

@@ -116,13 +116,29 @@ Item {
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent" color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
IconImage { IconImage {
id: iconImg
anchors.centerIn: parent anchors.centerIn: parent
width: 16 width: Theme.barIconSize(root.barThickness)
height: 16 height: Theme.barIconSize(root.barThickness)
source: delegateRoot.iconSource source: delegateRoot.iconSource
asynchronous: true asynchronous: true
smooth: true smooth: true
mipmap: true mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || ""
if (!itemId) {
return "?"
}
return itemId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
} }
} }
@@ -130,6 +146,7 @@ Item {
id: trayItemArea id: trayItemArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {
@@ -202,13 +219,29 @@ Item {
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent" color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
IconImage { IconImage {
id: iconImg
anchors.centerIn: parent anchors.centerIn: parent
width: 16 width: Theme.barIconSize(root.barThickness)
height: 16 height: Theme.barIconSize(root.barThickness)
source: delegateRoot.iconSource source: delegateRoot.iconSource
asynchronous: true asynchronous: true
smooth: true smooth: true
mipmap: true mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || ""
if (!itemId) {
return "?"
}
return itemId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
} }
} }
@@ -216,6 +249,7 @@ Item {
id: trayItemArea id: trayItemArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {

View File

@@ -25,10 +25,18 @@ BasePill {
DankIcon { DankIcon {
id: icon id: icon
name: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off") name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
size: Theme.barIconSize(root.barThickness, -4) size: Theme.barIconSize(root.barThickness, -4)
color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceText color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceText
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
anchors.centerIn: parent anchors.centerIn: parent
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.InOutQuad
}
}
} }
} }
} }
@@ -44,8 +52,9 @@ BasePill {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
enabled: !DMSNetworkService.isBusy
onPressed: { onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) { if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0) const globalPos = root.visualContent.mapToGlobal(0, 0)
@@ -65,9 +74,15 @@ BasePill {
} else { } else {
const names = DMSNetworkService.activeNames || [] const names = DMSNetworkService.activeNames || []
if (names.length <= 1) { if (names.length <= 1) {
tooltipText = "VPN Connected • " + (names[0] || "") const name = names[0] || ""
const maxLength = 25
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
tooltipText = "VPN Connected • " + displayName
} else { } else {
tooltipText = "VPN Connected • " + names[0] + " +" + (names.length - 1) const name = names[0]
const maxLength = 20
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
tooltipText = "VPN Connected • " + displayName + " +" + (names.length - 1)
} }
} }

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.I3
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -17,17 +18,37 @@ Item {
property real barThickness: 48 property real barThickness: 48
property var hyprlandOverviewLoader: null property var hyprlandOverviewLoader: null
property var parentScreen: null property var parentScreen: null
property int _desktopEntriesUpdateTrigger: 0
readonly property var sortedToplevels: { readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
} }
Connections {
target: DesktopEntries
function onApplicationsChanged() {
_desktopEntriesUpdateTrigger++
}
}
property int currentWorkspace: { property int currentWorkspace: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
return getNiriActiveWorkspace() return getNiriActiveWorkspace()
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
return getHyprlandActiveWorkspace() return getHyprlandActiveWorkspace()
} else if (CompositorService.isDwl) {
const activeTags = getDwlActiveTags()
return activeTags.length > 0 ? activeTags[0] : 0
} else if (CompositorService.isSway) {
return getSwayActiveWorkspace()
} }
return 1 return 1
} }
property var dwlActiveTags: {
if (CompositorService.isDwl) {
return getDwlActiveTags()
}
return []
}
property var workspaceList: { property var workspaceList: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
const baseList = getNiriWorkspaces() const baseList = getNiriWorkspaces()
@@ -35,14 +56,44 @@ Item {
} }
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
const baseList = getHyprlandWorkspaces() const baseList = getHyprlandWorkspaces()
// Filter out special workspaces
const filteredList = baseList.filter(ws => ws.id > -1) const filteredList = baseList.filter(ws => ws.id > -1)
return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList
} }
if (CompositorService.isDwl) {
const baseList = getDwlTags()
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
}
if (CompositorService.isSway) {
const baseList = getSwayWorkspaces()
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
}
return [1] return [1]
} }
function getSwayWorkspaces() {
const workspaces = I3.workspaces?.values || []
if (workspaces.length === 0) return [{"num": 1}]
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return workspaces.slice().sort((a, b) => a.num - b.num)
}
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}]
}
function getSwayActiveWorkspace() {
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
return focusedWs ? focusedWs.num : 1
}
const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true)
return focusedWs ? focusedWs.num : 1
}
function getWorkspaceIcons(ws) { function getWorkspaceIcons(ws) {
_desktopEntriesUpdateTrigger
if (!SettingsData.showWorkspaceApps || !ws) { if (!SettingsData.showWorkspaceApps || !ws) {
return [] return []
} }
@@ -60,15 +111,35 @@ Item {
targetWorkspaceId = workspace.id targetWorkspaceId = workspace.id
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
targetWorkspaceId = ws.id !== undefined ? ws.id : ws targetWorkspaceId = ws.id !== undefined ? ws.id : ws
} else if (CompositorService.isDwl) {
if (typeof ws !== "object" || ws.tag === undefined) {
return []
}
targetWorkspaceId = ws.tag
} else if (CompositorService.isSway) {
targetWorkspaceId = ws.num !== undefined ? ws.num : ws
} else { } else {
return [] return []
} }
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels
const byApp = {} const byApp = {}
const isActiveWs = CompositorService.isNiri ? NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active) : targetWorkspaceId === root.currentWorkspace let isActiveWs = false
if (CompositorService.isNiri) {
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active)
} else if (CompositorService.isSway) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false
} else if (CompositorService.isDwl) {
const output = DwlService.getOutputState(root.screenName)
if (output && output.tags) {
const tag = output.tags.find(t => t.tag === targetWorkspaceId)
isActiveWs = tag ? (tag.state === 1) : false
}
} else {
isActiveWs = targetWorkspaceId === root.currentWorkspace
}
wins.forEach((w, i) => { wins.forEach((w, i) => {
if (!w) { if (!w) {
@@ -78,8 +149,9 @@ Item {
let winWs = null let winWs = null
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
winWs = w.workspace_id winWs = w.workspace_id
} else if (CompositorService.isSway) {
winWs = w.workspace?.num
} else { } else {
// For Hyprland, we need to find the corresponding Hyprland toplevel to get workspace
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []) const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || [])
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w) const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w)
winWs = hyprToplevel?.workspace?.id winWs = hyprToplevel?.workspace?.id
@@ -101,14 +173,14 @@ Item {
"type": "icon", "type": "icon",
"icon": icon, "icon": icon,
"isSteamApp": isSteamApp, "isSteamApp": isSteamApp,
"active": !!(w.activated || (CompositorService.isNiri && w.is_focused)), "active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
"count": 1, "count": 1,
"windowId": w.address || w.id, "windowId": w.address || w.id,
"fallbackText": w.appId || w.class || w.title || "" "fallbackText": w.appId || w.class || w.title || ""
} }
} else { } else {
byApp[key].count++ byApp[key].count++
if (w.activated || (CompositorService.isNiri && w.is_focused)) { if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) {
byApp[key].active = true byApp[key].active = true
} }
} }
@@ -119,10 +191,16 @@ Item {
function padWorkspaces(list) { function padWorkspaces(list) {
const padded = list.slice() const padded = list.slice()
const placeholder = CompositorService.isHyprland ? { let placeholder
"id": -1, if (CompositorService.isHyprland) {
"name": "" placeholder = {"id": -1, "name": ""}
} : -1 } else if (CompositorService.isDwl) {
placeholder = {"tag": -1}
} else if (CompositorService.isSway) {
placeholder = {"num": -1}
} else {
placeholder = -1
}
while (padded.length < 3) { while (padded.length < 3) {
padded.push(placeholder) padded.push(placeholder)
} }
@@ -190,7 +268,6 @@ Item {
return Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id : 1 return Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id : 1
} }
// Find the monitor object for this screen
const monitors = Hyprland.monitors?.values || [] const monitors = Hyprland.monitors?.values || []
const currentMonitor = monitors.find(monitor => monitor.name === root.screenName) const currentMonitor = monitors.find(monitor => monitor.name === root.screenName)
@@ -198,10 +275,44 @@ Item {
return 1 return 1
} }
// Use the monitor's active workspace ID (like original config)
return currentMonitor.activeWorkspace?.id ?? 1 return currentMonitor.activeWorkspace?.id ?? 1
} }
function getDwlTags() {
if (!DwlService.dwlAvailable) {
return [{"tag": 0}, {"tag": 1}]
}
const output = DwlService.getOutputState(root.screenName)
if (!output || !output.tags || output.tags.length === 0) {
return [{"tag": 0}]
}
if (SettingsData.dwlShowAllTags) {
return output.tags.map(tag => ({"tag": tag.tag, "state": tag.state, "clients": tag.clients, "focused": tag.focused}))
}
const visibleTagIndices = DwlService.getVisibleTags(root.screenName)
return visibleTagIndices.map(tagIndex => {
const tagData = output.tags.find(t => t.tag === tagIndex)
return {
"tag": tagIndex,
"state": tagData?.state ?? 0,
"clients": tagData?.clients ?? 0,
"focused": tagData?.focused ?? false
}
})
}
function getDwlActiveTags() {
if (!DwlService.dwlAvailable) {
return [0]
}
const activeTags = DwlService.getActiveTags(root.screenName)
return activeTags.length > 0 ? activeTags : [0]
}
readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2) readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
@@ -210,6 +321,10 @@ Item {
return root.workspaceList.filter(ws => { return root.workspaceList.filter(ws => {
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
return ws && ws.id !== -1 return ws && ws.id !== -1
} else if (CompositorService.isDwl) {
return ws && ws.tag !== -1
} else if (CompositorService.isSway) {
return ws && ws.num !== -1
} }
return ws !== -1 return ws !== -1
}) })
@@ -246,12 +361,42 @@ Item {
} }
Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`) Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`)
} else if (CompositorService.isDwl) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag)
} else if (CompositorService.isSway) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){}
} }
} }
width: isVertical ? barThickness : visualWidth width: isVertical ? barThickness : visualWidth
height: isVertical ? visualHeight : barThickness height: isVertical ? visualHeight : barThickness
visible: CompositorService.isNiri || CompositorService.isHyprland visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway
Rectangle { Rectangle {
id: visualBackground id: visualBackground
@@ -271,104 +416,11 @@ Item {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
property real scrollAccumulator: 0
property real touchpadThreshold: 500
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.RightButton && CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { if (mouse.button === Qt.RightButton && CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
} }
} }
onWheel: wheel => {
const deltaY = wheel.angleDelta.y
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
const direction = deltaY < 0 ? 1 : -1
if (isMouseWheel) {
if (!SettingsData.workspaceScrolling || !CompositorService.isNiri) {
switchWorkspace(direction)
}
else {
const windows = root.sortedToplevels;
if (windows.length < 2) {
return;
}
let currentIndex = -1;
for (let i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i;
break;
}
}
let nextIndex;
if (deltaY < 0) {
if (currentIndex === -1) {
nextIndex = 0;
} else {
nextIndex = currentIndex +1;
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length -1;
} else {
nextIndex = currentIndex - 1
}
}
const nextWindow = windows[nextIndex];
if (nextWindow) {
nextWindow.activate();
}
}
} else {
scrollAccumulator += deltaY
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
const touchDirection = scrollAccumulator < 0 ? 1 : -1
if (!SettingsData.workspaceScrolling || !CompositorService.isNiri) {
switchWorkspace(touchDirection)
}
else {
const windows = root.sortedToplevels;
if (windows.length < 2) {
return;
}
let currentIndex = -1;
for (let i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i;
break;
}
}
let nextIndex;
if (deltaY < 0) {
if (currentIndex === -1) {
nextIndex = 0;
} else {
nextIndex = currentIndex +1;
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length -1;
} else {
nextIndex = currentIndex - 1
}
}
const nextWindow = windows[nextIndex];
if (nextWindow) {
nextWindow.activate();
}
}
scrollAccumulator = 0
}
}
wheel.accepted = true
}
} }
Flow { Flow {
@@ -387,12 +439,20 @@ Item {
property bool isActive: { property bool isActive: {
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
return modelData && modelData.id === root.currentWorkspace return modelData && modelData.id === root.currentWorkspace
} else if (CompositorService.isDwl) {
return modelData && root.dwlActiveTags.includes(modelData.tag)
} else if (CompositorService.isSway) {
return modelData && modelData.num === root.currentWorkspace
} }
return modelData === root.currentWorkspace return modelData === root.currentWorkspace
} }
property bool isPlaceholder: { property bool isPlaceholder: {
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
return modelData && modelData.id === -1 return modelData && modelData.id === -1
} else if (CompositorService.isDwl) {
return modelData && modelData.tag === -1
} else if (CompositorService.isSway) {
return modelData && modelData.num === -1
} }
return modelData === -1 return modelData === -1
} }
@@ -403,8 +463,11 @@ Item {
property bool isUrgent: { property bool isUrgent: {
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
return modelData?.urgent ?? false return modelData?.urgent ?? false
} } else if (CompositorService.isNiri) {
if (CompositorService.isNiri) { return loadedIsUrgent
} else if (CompositorService.isDwl) {
return modelData?.state === 2
} else if (CompositorService.isSway) {
return loadedIsUrgent return loadedIsUrgent
} }
return false return false
@@ -447,15 +510,29 @@ Item {
hoverEnabled: !isPlaceholder hoverEnabled: !isPlaceholder
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isPlaceholder enabled: !isPlaceholder
onClicked: { acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (isPlaceholder) { if (isPlaceholder) {
return return
} }
const isRightClick = mouse.button === Qt.RightButton
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
NiriService.switchToWorkspace(modelData - 1) NiriService.switchToWorkspace(modelData - 1)
} else if (CompositorService.isHyprland && modelData?.id) { } else if (CompositorService.isHyprland && modelData?.id) {
Hyprland.dispatch(`workspace ${modelData.id}`) Hyprland.dispatch(`workspace ${modelData.id}`)
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
console.log("DWL click - tag:", modelData.tag, "rightClick:", isRightClick)
if (isRightClick) {
console.log("Calling toggleTag")
DwlService.toggleTag(root.screenName, modelData.tag)
} else {
console.log("Calling switchToTag")
DwlService.switchToTag(root.screenName, modelData.tag)
}
} else if (CompositorService.isSway && modelData?.num) {
try { I3.dispatch(`workspace number ${modelData.num}`) } catch(_){}
} }
} }
} }
@@ -478,9 +555,13 @@ Item {
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null; wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
wsData = modelData; wsData = modelData;
} else if (CompositorService.isDwl) {
wsData = modelData;
} else if (CompositorService.isSway) {
wsData = modelData;
} }
delegateRoot.loadedWorkspaceData = wsData; delegateRoot.loadedWorkspaceData = wsData;
delegateRoot.loadedIsUrgent = wsData?.is_urgent ?? false; delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
var icData = null; var icData = null;
if (wsData?.name) { if (wsData?.name) {
@@ -490,7 +571,11 @@ Item {
delegateRoot.loadedHasIcon = icData !== null; delegateRoot.loadedHasIcon = icData !== null;
if (SettingsData.showWorkspaceApps) { if (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else {
delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData)); delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
}
} else { } else {
delegateRoot.loadedIcons = []; delegateRoot.loadedIcons = [];
} }
@@ -512,8 +597,14 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
border.width: isUrgent && !isActive ? 2 : 0 border.width: {
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0) if (isUrgent && !isActive) return 2
return 0
}
border.color: {
if (isUrgent && !isActive) return Theme.error
return Theme.withAlpha(Theme.error, 0)
}
Behavior on width { Behavior on width {
enabled: (!SettingsData.showWorkspaceApps || SettingsData.maxWorkspaceIcons <= 3) enabled: (!SettingsData.showWorkspaceApps || SettingsData.maxWorkspaceIcons <= 3)
@@ -545,6 +636,13 @@ Item {
} }
} }
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Loader { Loader {
id: appIconsLoader id: appIconsLoader
anchors.fill: parent anchors.fill: parent
@@ -735,11 +833,29 @@ Item {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: { text: {
const isPlaceholder = CompositorService.isHyprland ? (modelData?.id === -1) : (modelData === -1) let isPlaceholder
if (CompositorService.isHyprland) {
isPlaceholder = modelData?.id === -1
} else if (CompositorService.isDwl) {
isPlaceholder = modelData?.tag === -1
} else if (CompositorService.isSway) {
isPlaceholder = modelData?.num === -1
} else {
isPlaceholder = modelData === -1
}
if (isPlaceholder) { if (isPlaceholder) {
return index + 1 return index + 1
} }
return CompositorService.isHyprland ? (modelData?.id || "") : (modelData - 1);
if (CompositorService.isHyprland) {
return modelData?.id || ""
} else if (CompositorService.isDwl) {
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""
} else if (CompositorService.isSway) {
return modelData?.num || ""
}
return modelData - 1
} }
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness) font.pixelSize: Theme.barTextSize(barThickness)
@@ -767,6 +883,16 @@ Item {
function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() } function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() }
function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() } function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() }
} }
Connections {
target: DwlService
enabled: CompositorService.isDwl
function onStateChanged() { delegateRoot.updateAllData() }
}
Connections {
target: I3.workspaces
enabled: CompositorService.isSway
function onValuesChanged() { delegateRoot.updateAllData() }
}
} }
} }
} }

View File

@@ -49,12 +49,16 @@ DankPopout {
property bool __contentReady: false property bool __contentReady: false
function __tryFocusOnce() { function __tryFocusOnce() {
if (!__focusArmed) return if (!__focusArmed)
return
const win = root.window const win = root.window
if (!win || !win.visible) return if (!win || !win.visible)
if (!contentLoader.item) return return
if (!contentLoader.item)
return
if (win.requestActivate) win.requestActivate() if (win.requestActivate)
win.requestActivate()
contentLoader.item.forceActiveFocus(Qt.TabFocusReason) contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
if (contentLoader.item.activeFocus) if (contentLoader.item.activeFocus)
@@ -78,14 +82,18 @@ DankPopout {
target: contentLoader target: contentLoader
function onLoaded() { function onLoaded() {
__contentReady = true __contentReady = true
if (__focusArmed) __tryFocusOnce() if (__focusArmed)
__tryFocusOnce()
} }
} }
Connections { Connections {
target: root.window ? root.window : null target: root.window ? root.window : null
enabled: !!root.window enabled: !!root.window
function onVisibleChanged() { if (__focusArmed) __tryFocusOnce() } function onVisibleChanged() {
if (__focusArmed)
__tryFocusOnce()
}
} }
onBackgroundClicked: { onBackgroundClicked: {
@@ -111,14 +119,14 @@ DankPopout {
target: root target: root
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(function() { Qt.callLater(function () {
mainContainer.forceActiveFocus() mainContainer.forceActiveFocus()
}) })
} }
} }
} }
Keys.onPressed: function(event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.dashVisible = false root.dashVisible = false
event.accepted = true event.accepted = true
@@ -216,32 +224,43 @@ DankPopout {
} }
model: { model: {
let tabs = [ let tabs = [{
{ icon: "dashboard", text: I18n.tr("Overview") }, "icon": "dashboard",
{ icon: "music_note", text: I18n.tr("Media") }, "text": I18n.tr("Overview")
{ icon: "wallpaper", text: I18n.tr("Wallpapers") } }, {
] "icon": "music_note",
"text": I18n.tr("Media")
}, {
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}]
if (SettingsData.weatherEnabled) { if (SettingsData.weatherEnabled) {
tabs.push({ icon: "wb_sunny", text: I18n.tr("Weather") }) tabs.push({
"icon": "wb_sunny",
"text": I18n.tr("Weather")
})
} }
tabs.push({ icon: "settings", text: I18n.tr("Settings"), isAction: true }) tabs.push({
"icon": "settings",
"text": I18n.tr("Settings"),
"isAction": true
})
return tabs return tabs
} }
onTabClicked: function(index) { onTabClicked: function (index) {
root.currentTabIndex = index root.currentTabIndex = index
} }
onActionTriggered: function(index) { onActionTriggered: function (index) {
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3 let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
if (index === settingsIndex) { if (index === settingsIndex) {
dashVisible = false dashVisible = false
settingsModal.show() settingsModal.show()
} }
} }
} }
Item { Item {
@@ -253,10 +272,14 @@ DankPopout {
id: pages id: pages
width: parent.width width: parent.width
implicitHeight: { implicitHeight: {
if (currentIndex === 0) return overviewTab.implicitHeight if (currentIndex === 0)
if (currentIndex === 1) return mediaTab.implicitHeight return overviewTab.implicitHeight
if (currentIndex === 2) return wallpaperTab.implicitHeight if (currentIndex === 1)
if (SettingsData.weatherEnabled && currentIndex === 3) return weatherTab.implicitHeight return mediaTab.implicitHeight
if (currentIndex === 2)
return wallpaperTab.implicitHeight
if (SettingsData.weatherEnabled && currentIndex === 3)
return weatherTab.implicitHeight
return overviewTab.implicitHeight return overviewTab.implicitHeight
} }
currentIndex: root.currentTabIndex currentIndex: root.currentTabIndex
@@ -264,6 +287,10 @@ DankPopout {
OverviewTab { OverviewTab {
id: overviewTab id: overviewTab
onCloseDash: {
root.dashVisible = false
}
onSwitchToWeatherTab: { onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) { if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 3 tabBar.currentIndex = 3
@@ -286,6 +313,7 @@ DankPopout {
active: root.currentTabIndex === 2 active: root.currentTabIndex === 2
tabBarItem: tabBar tabBarItem: tabBar
keyForwardTarget: mainContainer keyForwardTarget: mainContainer
targetScreen: root.triggerScreen
} }
WeatherTab { WeatherTab {

View File

@@ -13,6 +13,8 @@ Rectangle {
property var selectedDateEvents: [] property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0 property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
signal closeDash()
function weekStartJs() { function weekStartJs() {
return Qt.locale().firstDayOfWeek % 7 return Qt.locale().firstDayOfWeek % 7
} }
@@ -428,24 +430,12 @@ Rectangle {
if (modelData.url && modelData.url !== "") { if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) { if (Qt.openUrlExternally(modelData.url) === false) {
console.warn("Failed to open URL: " + modelData.url) console.warn("Failed to open URL: " + modelData.url)
} else {
root.closeDash()
} }
} }
} }
} }
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
} }
} }

View File

@@ -14,6 +14,7 @@ Item {
signal switchToWeatherTab() signal switchToWeatherTab()
signal switchToMediaTab() signal switchToMediaTab()
signal closeDash()
Item { Item {
anchors.fill: parent anchors.fill: parent
@@ -58,6 +59,8 @@ Item {
y: 100 + Theme.spacingM y: 100 + Theme.spacingM
width: parent.width * 0.6 width: parent.width * 0.6
height: 300 height: 300
onCloseDash: root.closeDash()
} }
// Media - bottom right (narrow and taller) // Media - bottom right (narrow and taller)

View File

@@ -29,9 +29,26 @@ Item {
property bool enableAnimation: false property bool enableAnimation: false
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation) property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
property string selectedFileName: "" property string selectedFileName: ""
property var targetScreen: null
property string targetScreenName: targetScreen ? targetScreen.name : ""
signal requestTabChange(int newIndex) signal requestTabChange(int newIndex)
function getCurrentWallpaper() {
if (SessionData.perMonitorWallpaper && targetScreenName) {
return SessionData.getMonitorWallpaper(targetScreenName)
}
return SessionData.wallpaperPath
}
function setCurrentWallpaper(path) {
if (SessionData.perMonitorWallpaper && targetScreenName) {
SessionData.setMonitorWallpaper(targetScreenName, path)
} else {
SessionData.setWallpaper(path)
}
}
onCurrentPageChanged: { onCurrentPageChanged: {
if (currentPage !== lastPage) { if (currentPage !== lastPage) {
enableAnimation = false enableAnimation = false
@@ -71,7 +88,7 @@ Item {
if (absoluteIndex < wallpaperFolderModel.count) { if (absoluteIndex < wallpaperFolderModel.count) {
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath") const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath")
if (filePath) { if (filePath) {
SessionData.setWallpaper(filePath.toString().replace(/^file:\/\//, '')) setCurrentWallpaper(filePath.toString().replace(/^file:\/\//, ''))
} }
} }
} }
@@ -151,32 +168,39 @@ Item {
} }
function setInitialSelection() { function setInitialSelection() {
if (!SessionData.wallpaperPath || wallpaperFolderModel.count === 0) { const currentWallpaper = getCurrentWallpaper()
if (!currentWallpaper || wallpaperFolderModel.count === 0) {
gridIndex = 0 gridIndex = 0
updateSelectedFileName() updateSelectedFileName()
Qt.callLater(() => { enableAnimation = true }) Qt.callLater(() => {
enableAnimation = true
})
return return
} }
for (let i = 0; i < wallpaperFolderModel.count; i++) { for (var i = 0; i < wallpaperFolderModel.count; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath") const filePath = wallpaperFolderModel.get(i, "filePath")
if (filePath && filePath.toString().replace(/^file:\/\//, '') === SessionData.wallpaperPath) { if (filePath && filePath.toString().replace(/^file:\/\//, '') === currentWallpaper) {
const targetPage = Math.floor(i / itemsPerPage) const targetPage = Math.floor(i / itemsPerPage)
const targetIndex = i % itemsPerPage const targetIndex = i % itemsPerPage
currentPage = targetPage currentPage = targetPage
gridIndex = targetIndex gridIndex = targetIndex
updateSelectedFileName() updateSelectedFileName()
Qt.callLater(() => { enableAnimation = true }) Qt.callLater(() => {
enableAnimation = true
})
return return
} }
} }
gridIndex = 0 gridIndex = 0
updateSelectedFileName() updateSelectedFileName()
Qt.callLater(() => { enableAnimation = true }) Qt.callLater(() => {
enableAnimation = true
})
} }
function loadWallpaperDirectory() { function loadWallpaperDirectory() {
const currentWallpaper = SessionData.wallpaperPath const currentWallpaper = getCurrentWallpaper()
if (!currentWallpaper || currentWallpaper.startsWith("#") || currentWallpaper.startsWith("we:")) { if (!currentWallpaper || currentWallpaper.startsWith("#") || currentWallpaper.startsWith("we:")) {
if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") { if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") {
@@ -216,6 +240,19 @@ Item {
setInitialSelection() setInitialSelection()
} }
} }
function onMonitorWallpapersChanged() {
loadWallpaperDirectory()
if (visible && active) {
setInitialSelection()
}
}
}
onTargetScreenNameChanged: {
loadWallpaperDirectory()
if (visible && active) {
setInitialSelection()
}
} }
Connections { Connections {
@@ -267,9 +304,9 @@ Item {
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
allowStacking: true allowStacking: true
onFileSelected: (path) => { onFileSelected: path => {
const cleanPath = path.replace(/^file:\/\//, '') const cleanPath = path.replace(/^file:\/\//, '')
SessionData.setWallpaper(cleanPath) setCurrentWallpaper(cleanPath)
const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/')) const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'))
if (dirPath) { if (dirPath) {
@@ -327,7 +364,7 @@ Item {
const startIndex = currentPage * itemsPerPage const startIndex = currentPage * itemsPerPage
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count) const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count)
const items = [] const items = []
for (let i = startIndex; i < endIndex; i++) { for (var i = startIndex; i < endIndex; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath") const filePath = wallpaperFolderModel.get(i, "filePath")
if (filePath) { if (filePath) {
items.push(filePath.toString().replace(/^file:\/\//, '')) items.push(filePath.toString().replace(/^file:\/\//, ''))
@@ -369,7 +406,7 @@ Item {
height: wallpaperGrid.cellHeight height: wallpaperGrid.cellHeight
property string wallpaperPath: modelData || "" property string wallpaperPath: modelData || ""
property bool isSelected: SessionData.wallpaperPath === modelData property bool isSelected: getCurrentWallpaper() === modelData
Rectangle { Rectangle {
id: wallpaperCard id: wallpaperCard
@@ -437,7 +474,7 @@ Item {
onClicked: { onClicked: {
gridIndex = index gridIndex = index
if (modelData) { if (modelData) {
SessionData.setWallpaper(modelData) setCurrentWallpaper(modelData)
} }
} }
} }

View File

@@ -213,13 +213,6 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
} }
MouseArea {
visible: model.type === "separator"
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
DockAppButton { DockAppButton {
id: button id: button
visible: model.type !== "separator" visible: model.type !== "separator"

View File

@@ -166,6 +166,10 @@ NIRI_EOF
cat > "$TEMP_CONFIG" << HYPRLAND_EOF cat > "$TEMP_CONFIG" << HYPRLAND_EOF
env = DMS_RUN_GREETER,1 env = DMS_RUN_GREETER,1
misc {
disable_hyprland_logo = true
}
exec = sh -c "$QS_CMD; hyprctl dispatch exit" exec = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"

View File

@@ -11,6 +11,7 @@ Scope {
id: root id: root
property bool lockSecured: false property bool lockSecured: false
property bool unlockInProgress: false
readonly property alias passwd: passwd readonly property alias passwd: passwd
readonly property alias fprint: fprint readonly property alias fprint: fprint
@@ -50,7 +51,11 @@ Scope {
onCompleted: res => { onCompleted: res => {
if (res === PamResult.Success) { if (res === PamResult.Success) {
if (!root.unlockInProgress) {
root.unlockInProgress = true;
fprint.abort();
root.unlockRequested(); root.unlockRequested();
}
return; return;
} }
@@ -92,7 +97,11 @@ Scope {
return; return;
if (res === PamResult.Success) { if (res === PamResult.Success) {
if (!root.unlockInProgress) {
root.unlockInProgress = true;
passwd.abort();
root.unlockRequested(); root.unlockRequested();
}
return; return;
} }
@@ -162,8 +171,11 @@ Scope {
root.state = ""; root.state = "";
root.fprintState = ""; root.fprintState = "";
root.lockMessage = ""; root.lockMessage = "";
root.unlockInProgress = false;
} else { } else {
fprint.abort(); fprint.abort();
passwd.abort();
root.unlockInProgress = false;
} }
} }

View File

@@ -9,6 +9,59 @@ Item {
id: aboutTab id: aboutTab
property bool isHyprland: CompositorService.isHyprland property bool isHyprland: CompositorService.isHyprland
property bool isNiri: CompositorService.isNiri
property bool isSway: CompositorService.isSway
property bool isDwl: CompositorService.isDwl
property string compositorName: {
if (isHyprland) return "hyprland"
if (isSway) return "sway"
if (isDwl) return "mangowc"
return "niri"
}
property string compositorLogo: {
if (isHyprland) return "/assets/hyprland.svg"
if (isSway) return "/assets/sway.svg"
if (isDwl) return "/assets/mango.png"
return "/assets/niri.svg"
}
property string compositorUrl: {
if (isHyprland) return "https://hypr.land"
if (isSway) return "https://swaywm.org"
if (isDwl) return "https://github.com/DreamMaoMao/mangowc"
return "https://github.com/YaLTeR/niri"
}
property string compositorTooltip: {
if (isHyprland) return "Hyprland Website"
if (isSway) return "Sway Website"
if (isDwl) return "mangowc GitHub"
return "niri GitHub"
}
property string dmsDiscordUrl: "https://discord.gg/vT8Sfjy7sx"
property string dmsDiscordTooltip: "niri/dms Discord"
property string compositorDiscordUrl: {
if (isHyprland) return "https://discord.com/invite/hQ9XvMUjjr"
if (isDwl) return "https://discord.gg/CPjbDxesh5"
return ""
}
property string compositorDiscordTooltip: {
if (isHyprland) return "Hyprland Discord Server"
if (isDwl) return "mangowc Discord Server"
return ""
}
property string redditUrl: "https://reddit.com/r/niri"
property string redditTooltip: "r/niri Subreddit"
property bool showMatrix: isNiri && !isHyprland && !isSway && !isDwl
property bool showCompositorDiscord: isHyprland || isDwl
property bool showReddit: isNiri && !isHyprland && !isSway && !isDwl
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
@@ -69,14 +122,19 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
height: 24 height: 24
width: { width: {
if (isHyprland) { let baseWidth = compositorButton.width + dmsDiscordButton.width + Theme.spacingM
return compositorButton.width + discordButton.width + Theme.spacingM + redditButton.width + Theme.spacingM if (showMatrix) {
} else { baseWidth += matrixButton.width + 4
return compositorButton.width + matrixButton.width + 4 + discordButton.width + Theme.spacingM + redditButton.width + Theme.spacingM
} }
if (showCompositorDiscord) {
baseWidth += compositorDiscordButton.width + Theme.spacingM
}
if (showReddit) {
baseWidth += redditButton.width + Theme.spacingM
}
return baseWidth
} }
// Compositor logo (Niri or Hyprland)
Item { Item {
id: compositorButton id: compositorButton
width: 24 width: 24
@@ -86,14 +144,14 @@ Item {
x: 0 x: 0
property bool hovered: false property bool hovered: false
property string tooltipText: isHyprland ? "Hyprland Website" : "niri GitHub" property string tooltipText: compositorTooltip
Image { Image {
anchors.fill: parent anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace( source: Qt.resolvedUrl(".").toString().replace(
"file://", "").replace( "file://", "").replace(
"/Modules/Settings/", "/Modules/Settings/",
"") + (isHyprland ? "/assets/hyprland.svg" : "/assets/niri.svg") "") + compositorLogo
sourceSize: Qt.size(24, 24) sourceSize: Qt.size(24, 24)
smooth: true smooth: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
@@ -105,18 +163,16 @@ Item {
hoverEnabled: true hoverEnabled: true
onEntered: parent.hovered = true onEntered: parent.hovered = true
onExited: parent.hovered = false onExited: parent.hovered = false
onClicked: Qt.openUrlExternally( onClicked: Qt.openUrlExternally(compositorUrl)
isHyprland ? "https://hypr.land" : "https://github.com/YaLTeR/niri")
} }
} }
// Matrix button (only for Niri)
Item { Item {
id: matrixButton id: matrixButton
width: 30 width: 30
height: 24 height: 24
x: compositorButton.x + compositorButton.width + 4 x: compositorButton.x + compositorButton.width + 4
visible: !isHyprland visible: showMatrix
property bool hovered: false property bool hovered: false
property string tooltipText: "niri Matrix Chat" property string tooltipText: "niri Matrix Chat"
@@ -149,16 +205,15 @@ Item {
} }
} }
// Discord button
Item { Item {
id: discordButton id: dmsDiscordButton
width: 20 width: 20
height: 20 height: 20
x: isHyprland ? compositorButton.x + compositorButton.width + Theme.spacingM : matrixButton.x + matrixButton.width + Theme.spacingM x: showMatrix ? matrixButton.x + matrixButton.width + Theme.spacingM : compositorButton.x + compositorButton.width + Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
property bool hovered: false property bool hovered: false
property string tooltipText: isHyprland ? "Hyprland Discord Server" : "niri Discord Server" property string tooltipText: dmsDiscordTooltip
Image { Image {
anchors.fill: parent anchors.fill: parent
@@ -177,21 +232,52 @@ Item {
hoverEnabled: true hoverEnabled: true
onEntered: parent.hovered = true onEntered: parent.hovered = true
onExited: parent.hovered = false onExited: parent.hovered = false
onClicked: Qt.openUrlExternally( onClicked: Qt.openUrlExternally(dmsDiscordUrl)
isHyprland ? "https://discord.com/invite/hQ9XvMUjjr" : "https://discord.gg/vT8Sfjy7sx") }
}
Item {
id: compositorDiscordButton
width: 20
height: 20
x: dmsDiscordButton.x + dmsDiscordButton.width + Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: showCompositorDiscord
property bool hovered: false
property string tooltipText: compositorDiscordTooltip
Image {
anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace(
"file://", "").replace(
"/Modules/Settings/",
"") + "/assets/discord.svg"
sourceSize: Qt.size(20, 20)
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.hovered = true
onExited: parent.hovered = false
onClicked: Qt.openUrlExternally(compositorDiscordUrl)
} }
} }
// Reddit button
Item { Item {
id: redditButton id: redditButton
width: 20 width: 20
height: 20 height: 20
x: discordButton.x + discordButton.width + Theme.spacingM x: showCompositorDiscord ? compositorDiscordButton.x + compositorDiscordButton.width + Theme.spacingM : dmsDiscordButton.x + dmsDiscordButton.width + Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: showReddit
property bool hovered: false property bool hovered: false
property string tooltipText: isHyprland ? "r/hyprland Subreddit" : "r/niri Subreddit" property string tooltipText: redditTooltip
Image { Image {
anchors.fill: parent anchors.fill: parent
@@ -210,8 +296,7 @@ Item {
hoverEnabled: true hoverEnabled: true
onEntered: parent.hovered = true onEntered: parent.hovered = true
onExited: parent.hovered = false onExited: parent.hovered = false
onClicked: Qt.openUrlExternally( onClicked: Qt.openUrlExternally(redditUrl)
isHyprland ? "https://reddit.com/r/hyprland" : "https://reddit.com/r/niri")
} }
} }
} }
@@ -506,8 +591,9 @@ Item {
property var hoveredButton: { property var hoveredButton: {
if (compositorButton.hovered) return compositorButton if (compositorButton.hovered) return compositorButton
if (matrixButton.visible && matrixButton.hovered) return matrixButton if (matrixButton.visible && matrixButton.hovered) return matrixButton
if (discordButton.hovered) return discordButton if (dmsDiscordButton.hovered) return dmsDiscordButton
if (redditButton.hovered) return redditButton if (compositorDiscordButton.visible && compositorDiscordButton.hovered) return compositorDiscordButton
if (redditButton.visible && redditButton.hovered) return redditButton
return null return null
} }

View File

@@ -1781,6 +1781,9 @@ Item {
} else if (widgetId === "runningApps") { } else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode( SettingsData.setRunningAppsCompactMode(
value) value)
} else if (widgetId === "keyboard_layout_name") {
SettingsData.setKeyboardLayoutNameCompactMode(
value)
} }
} }
onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => { onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => {
@@ -1857,6 +1860,9 @@ Item {
} else if (widgetId === "runningApps") { } else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode( SettingsData.setRunningAppsCompactMode(
value) value)
} else if (widgetId === "keyboard_layout_name") {
SettingsData.setKeyboardLayoutNameCompactMode(
value)
} }
} }
onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => { onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => {
@@ -1933,6 +1939,9 @@ Item {
} else if (widgetId === "runningApps") { } else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode( SettingsData.setRunningAppsCompactMode(
value) value)
} else if (widgetId === "keyboard_layout_name") {
SettingsData.setKeyboardLayoutNameCompactMode(
value)
} }
} }
onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => { onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => {

View File

@@ -697,7 +697,7 @@ Item {
text: I18n.tr("Show on Last Display") text: I18n.tr("Show on Last Display")
description: I18n.tr("Always show when there's only one connected display") description: I18n.tr("Always show when there's only one connected display")
checked: displaysTab.getShowOnLastDisplay(parent.componentId) checked: displaysTab.getShowOnLastDisplay(parent.componentId)
visible: !displaysTab.getScreenPreferences(parent.componentId).includes("all") && ["dankBar", "dock", "notifications", "osd", "toast"].includes(parent.componentId) visible: !displaysTab.getScreenPreferences(parent.componentId).includes("all") && ["dankBar", "dock", "notifications", "osd", "toast", "notepad", "systemTray"].includes(parent.componentId)
onToggled: (checked) => { onToggled: (checked) => {
displaysTab.setShowOnLastDisplay(parent.componentId, checked); displaysTab.setShowOnLastDisplay(parent.componentId, checked);
} }

View File

@@ -87,9 +87,16 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
model: { model: {
const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo")] const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo")]
if (CompositorService.isNiri || CompositorService.isHyprland) { if (CompositorService.isNiri) {
const compositorName = CompositorService.isNiri ? "niri" : "Hyprland" modes.push("niri")
modes.push(compositorName) } else if (CompositorService.isHyprland) {
modes.push("Hyprland")
} else if (CompositorService.isDwl) {
modes.push("mango")
} else if (CompositorService.isSway) {
modes.push("Sway")
} else {
modes.push(I18n.tr("Compositor"))
} }
modes.push(I18n.tr("Custom")) modes.push(I18n.tr("Custom"))
return modes return modes
@@ -97,12 +104,8 @@ Item {
currentIndex: { currentIndex: {
if (SettingsData.launcherLogoMode === "apps") return 0 if (SettingsData.launcherLogoMode === "apps") return 0
if (SettingsData.launcherLogoMode === "os") return 1 if (SettingsData.launcherLogoMode === "os") return 1
if (SettingsData.launcherLogoMode === "compositor") { if (SettingsData.launcherLogoMode === "compositor") return 2
return (CompositorService.isNiri || CompositorService.isHyprland) ? 2 : -1 if (SettingsData.launcherLogoMode === "custom") return 3
}
if (SettingsData.launcherLogoMode === "custom") {
return (CompositorService.isNiri || CompositorService.isHyprland) ? 3 : 2
}
return 0 return 0
} }
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
@@ -111,15 +114,11 @@ Item {
SettingsData.setLauncherLogoMode("apps") SettingsData.setLauncherLogoMode("apps")
} else if (index === 1) { } else if (index === 1) {
SettingsData.setLauncherLogoMode("os") SettingsData.setLauncherLogoMode("os")
} else if (CompositorService.isNiri || CompositorService.isHyprland) { } else if (index === 2) {
if (index === 2) {
SettingsData.setLauncherLogoMode("compositor") SettingsData.setLauncherLogoMode("compositor")
} else if (index === 3) { } else if (index === 3) {
SettingsData.setLauncherLogoMode("custom") SettingsData.setLauncherLogoMode("custom")
} }
} else if (index === 2) {
SettingsData.setLauncherLogoMode("custom")
}
} }
} }
} }

View File

@@ -136,9 +136,7 @@ Item {
var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath
if (currentWallpaper && currentWallpaper.startsWith("we:")) { if (currentWallpaper && currentWallpaper.startsWith("we:")) {
var sceneId = currentWallpaper.substring(3) var sceneId = currentWallpaper.substring(3)
return StandardPaths.writableLocation(StandardPaths.HomeLocation) return StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960/" + sceneId + "/preview" + weExtensions[weExtIndex]
+ "/.local/share/Steam/steamapps/workshop/content/431960/"
+ sceneId + "/preview" + weExtensions[weExtIndex]
} }
return (currentWallpaper !== "" && !currentWallpaper.startsWith("#")) ? "file://" + currentWallpaper : "" return (currentWallpaper !== "" && !currentWallpaper.startsWith("#")) ? "file://" + currentWallpaper : ""
} }
@@ -147,10 +145,7 @@ Item {
if (currentWallpaper && currentWallpaper.startsWith("we:") && status === Image.Error) { if (currentWallpaper && currentWallpaper.startsWith("we:") && status === Image.Error) {
if (weExtIndex < weExtensions.length - 1) { if (weExtIndex < weExtensions.length - 1) {
weExtIndex++ weExtIndex++
source = StandardPaths.writableLocation(StandardPaths.HomeLocation) source = StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960/" + currentWallpaper.substring(3) + "/preview" + weExtensions[weExtIndex]
+ "/.local/share/Steam/steamapps/workshop/content/431960/"
+ currentWallpaper.substring(3)
+ "/preview" + weExtensions[weExtIndex]
} else { } else {
visible = false visible = false
} }
@@ -241,7 +236,6 @@ Item {
} }
} }
Rectangle { Rectangle {
width: 32 width: 32
height: 32 height: 32
@@ -263,7 +257,7 @@ Item {
var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath
PopoutService.colorPickerModal.selectedColor = currentWallpaper.startsWith("#") ? currentWallpaper : Theme.primary PopoutService.colorPickerModal.selectedColor = currentWallpaper.startsWith("#") ? currentWallpaper : Theme.primary
PopoutService.colorPickerModal.pickerTitle = "Choose Wallpaper Color" PopoutService.colorPickerModal.pickerTitle = "Choose Wallpaper Color"
PopoutService.colorPickerModal.onColorSelectedCallback = function(selectedColor) { PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
SessionData.setMonitorWallpaper(selectedMonitorName, selectedColor) SessionData.setMonitorWallpaper(selectedMonitorName, selectedColor)
} else { } else {
@@ -641,7 +635,7 @@ Item {
DankDropdown { DankDropdown {
id: monitorDropdown id: monitorDropdown
text: I18n.tr("Monitor") text: I18n.tr("Wallpaper Monitor")
description: I18n.tr("Select monitor to configure wallpaper") description: I18n.tr("Select monitor to configure wallpaper")
currentValue: selectedMonitorName || "No monitors" currentValue: selectedMonitorName || "No monitors"
options: { options: {
@@ -656,6 +650,36 @@ Item {
selectedMonitorName = value selectedMonitorName = value
} }
} }
DankDropdown {
id: matugenTargetDropdown
text: I18n.tr("Matugen Target Monitor")
description: I18n.tr("Monitor whose wallpaper drives dynamic theming colors")
currentValue: {
if (!SettingsData.matugenTargetMonitor || SettingsData.matugenTargetMonitor === "") {
var screens = Quickshell.screens
return screens.length > 0 ? screens[0].name + " (Default)" : "No monitors"
}
return SettingsData.matugenTargetMonitor
}
options: {
var screenNames = []
var screens = Quickshell.screens
for (var i = 0; i < screens.length; i++) {
var label = screens[i].name
if (i === 0 && (!SettingsData.matugenTargetMonitor || SettingsData.matugenTargetMonitor === "")) {
label += " (Default)"
}
screenNames.push(label)
}
return screenNames
}
onValueChanged: value => {
var cleanValue = value.replace(" (Default)", "")
SettingsData.setMatugenTargetMonitor(cleanValue)
}
}
} }
} }
@@ -956,7 +980,8 @@ Item {
text: I18n.tr("Transition Effect") text: I18n.tr("Transition Effect")
description: I18n.tr("Visual effect used when wallpaper changes") description: I18n.tr("Visual effect used when wallpaper changes")
currentValue: { currentValue: {
if (SessionData.wallpaperTransition === "random") return "Random" if (SessionData.wallpaperTransition === "random")
return "Random"
return SessionData.wallpaperTransition.charAt(0).toUpperCase() + SessionData.wallpaperTransition.slice(1) return SessionData.wallpaperTransition.charAt(0).toUpperCase() + SessionData.wallpaperTransition.slice(1)
} }
options: ["Random"].concat(SessionData.availableWallpaperTransitions.map(t => t.charAt(0).toUpperCase() + t.slice(1))) options: ["Random"].concat(SessionData.availableWallpaperTransitions.map(t => t.charAt(0).toUpperCase() + t.slice(1)))
@@ -1257,7 +1282,7 @@ Item {
showValue: false showValue: false
wheelEnabled: false wheelEnabled: false
onSliderValueChanged: (newValue) => { onSliderValueChanged: newValue => {
SettingsData.setAnimationSpeed(SettingsData.AnimationSpeed.Custom) SettingsData.setAnimationSpeed(SettingsData.AnimationSpeed.Custom)
SettingsData.setCustomAnimationDuration(newValue) SettingsData.setCustomAnimationDuration(newValue)
} }
@@ -1388,7 +1413,9 @@ Item {
id: personalizationMatugenPaletteDropdown id: personalizationMatugenPaletteDropdown
text: I18n.tr("Matugen Palette") text: I18n.tr("Matugen Palette")
description: I18n.tr("Select the palette algorithm used for wallpaper-based colors") description: I18n.tr("Select the palette algorithm used for wallpaper-based colors")
options: Theme.availableMatugenSchemes.map(function (option) { return option.label }) options: Theme.availableMatugenSchemes.map(function (option) {
return option.label
})
currentValue: Theme.getMatugenScheme(SettingsData.matugenScheme).label currentValue: Theme.getMatugenScheme(SettingsData.matugenScheme).label
enabled: Theme.matugenAvailable enabled: Theme.matugenAvailable
opacity: enabled ? 1 : 0.4 opacity: enabled ? 1 : 0.4

View File

@@ -0,0 +1,646 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property var allPlugins: []
property string searchQuery: ""
property var filteredPlugins: []
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property bool isLoading: false
property var parentModal: null
width: 600
height: 650
allowStacking: true
backgroundOpacity: 0
closeOnEscapeKey: false
function updateFilteredPlugins() {
var filtered = []
var query = searchQuery ? searchQuery.toLowerCase() : ""
for (var i = 0; i < allPlugins.length; i++) {
var plugin = allPlugins[i]
var isFirstParty = plugin.firstParty || false
if (!SessionData.showThirdPartyPlugins && !isFirstParty) {
continue
}
if (query.length > 0) {
var name = plugin.name ? plugin.name.toLowerCase() : ""
var description = plugin.description ? plugin.description.toLowerCase() : ""
var author = plugin.author ? plugin.author.toLowerCase() : ""
if (name.indexOf(query) !== -1 ||
description.indexOf(query) !== -1 ||
author.indexOf(query) !== -1) {
filtered.push(plugin)
}
} else {
filtered.push(plugin)
}
}
filteredPlugins = filtered
selectedIndex = -1
keyboardNavigationActive = false
}
function selectNext() {
if (filteredPlugins.length === 0) return
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, filteredPlugins.length - 1)
}
function selectPrevious() {
if (filteredPlugins.length === 0) return
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, -1)
if (selectedIndex === -1) {
keyboardNavigationActive = false
}
}
function installPlugin(pluginName) {
ToastService.showInfo("Installing plugin: " + pluginName)
DMSService.install(pluginName, response => {
if (response.error) {
ToastService.showError("Install failed: " + response.error)
} else {
ToastService.showInfo("Plugin installed: " + pluginName)
PluginService.scanPlugins()
refreshPlugins()
}
})
}
function refreshPlugins() {
isLoading = true
DMSService.listPlugins()
if (DMSService.apiVersion >= 8) {
DMSService.listInstalled()
}
}
function show() {
if (parentModal) {
parentModal.shouldHaveFocus = false
}
open()
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
}
function hide() {
close()
if (parentModal) {
parentModal.shouldHaveFocus = Qt.binding(() => {
return parentModal.shouldBeVisible
})
}
}
onOpened: {
refreshPlugins()
}
onDialogClosed: () => {
allPlugins = []
searchQuery = ""
filteredPlugins = []
selectedIndex = -1
keyboardNavigationActive = false
isLoading = false
}
onBackgroundClicked: () => {
hide()
}
content: Component {
FocusScope {
id: browserKeyHandler
property alias searchField: browserSearchField
anchors.fill: parent
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
} else if (event.key === Qt.Key_Down) {
root.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
root.selectPrevious()
event.accepted = true
}
}
Item {
id: browserContent
anchors.fill: parent
anchors.margins: Theme.spacingL
Item {
id: headerArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: Math.max(headerIcon.height, headerText.height, refreshButton.height, closeButton.height)
DankIcon {
id: headerIcon
name: "store"
size: Theme.iconSize
color: Theme.primary
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: headerText
text: I18n.tr("Browse Plugins")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.left: headerIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankButton {
id: thirdPartyButton
text: SessionData.showThirdPartyPlugins ? "Hide 3rd Party" : "Show 3rd Party"
iconName: SessionData.showThirdPartyPlugins ? "visibility_off" : "visibility"
height: 28
onClicked: {
if (SessionData.showThirdPartyPlugins) {
SessionData.setShowThirdPartyPlugins(false)
root.updateFilteredPlugins()
} else {
thirdPartyConfirmModal.open()
}
}
}
DankActionButton {
id: refreshButton
iconName: "refresh"
iconSize: 18
iconColor: Theme.primary
visible: !root.isLoading
onClicked: root.refreshPlugins()
}
DankActionButton {
id: closeButton
iconName: "close"
iconSize: Theme.iconSize - 2
iconColor: Theme.outline
onClicked: root.close()
}
}
}
StyledText {
id: descriptionText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerArea.bottom
anchors.topMargin: Theme.spacingM
text: I18n.tr("Install plugins from the DMS plugin registry")
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
wrapMode: Text.WordWrap
}
DankTextField {
id: browserSearchField
anchors.left: parent.left
anchors.right: parent.right
anchors.top: descriptionText.bottom
anchors.topMargin: Theme.spacingM
height: 48
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.fontSizeMedium
placeholderText: I18n.tr("Search plugins...")
text: root.searchQuery
focus: true
ignoreLeftRightKeys: true
keyForwardTargets: [browserKeyHandler]
onTextEdited: {
root.searchQuery = text
root.updateFilteredPlugins()
}
}
Item {
id: listArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: browserSearchField.bottom
anchors.topMargin: Theme.spacingM
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
Item {
anchors.fill: parent
visible: root.isLoading
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: "sync"
size: 48
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
RotationAnimator on rotation {
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
running: root.isLoading
}
}
StyledText {
text: I18n.tr("Loading plugins...")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
DankListView {
id: pluginBrowserList
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
spacing: Theme.spacingS
model: root.filteredPlugins
clip: true
visible: !root.isLoading
ScrollBar.vertical: DankScrollbar {
id: browserScrollbar
}
delegate: Rectangle {
width: pluginBrowserList.width
height: pluginDelegateColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
property bool isInstalled: modelData.installed || false
property bool isFirstParty: modelData.firstParty || false
color: isSelected ? Theme.primarySelected :
Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.3)
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: isSelected ? 2 : 1
Column {
id: pluginDelegateColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: modelData.icon || "extension"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - installButton.width - Theme.spacingM
spacing: 2
Row {
spacing: Theme.spacingXS
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
height: 16
width: firstPartyText.implicitWidth + Theme.spacingXS * 2
radius: 8
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4)
border.width: 1
visible: isFirstParty
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: firstPartyText
anchors.centerIn: parent
text: I18n.tr("official")
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.primary
font.weight: Font.Medium
}
}
Rectangle {
height: 16
width: thirdPartyText.implicitWidth + Theme.spacingXS * 2
radius: 8
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.15)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.4)
border.width: 1
visible: !isFirstParty
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: thirdPartyText
anchors.centerIn: parent
text: I18n.tr("3rd party")
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.warning
font.weight: Font.Medium
}
}
}
StyledText {
text: {
const author = "by " + (modelData.author || "Unknown")
const source = modelData.repo ? ` <a href="${modelData.repo}" style="text-decoration:none; color:${Theme.primary};">source</a>` : ""
return author + source
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
linkColor: Theme.primary
textFormat: Text.RichText
elide: Text.ElideRight
width: parent.width
onLinkActivated: url => Qt.openUrlExternally(url)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
}
Rectangle {
id: installButton
width: 80
height: 32
radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter
color: isInstalled ? Theme.surfaceVariant : Theme.primary
opacity: isInstalled ? 1 : (installMouseArea.containsMouse ? 0.9 : 1)
border.width: isInstalled ? 1 : 0
border.color: Theme.outline
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: isInstalled ? "check" : "download"
size: 14
color: isInstalled ? Theme.surfaceText : Theme.surface
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: isInstalled ? "Installed" : "Install"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: isInstalled ? Theme.surfaceText : Theme.surface
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: installMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: isInstalled ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isInstalled
onClicked: {
if (!isInstalled) {
root.installPlugin(modelData.name)
}
}
}
}
}
StyledText {
text: modelData.description || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width
wrapMode: Text.WordWrap
visible: modelData.description && modelData.description.length > 0
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: modelData.capabilities && modelData.capabilities.length > 0
Repeater {
model: modelData.capabilities || []
Rectangle {
height: 18
width: capabilityText.implicitWidth + Theme.spacingXS * 2
radius: 9
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 1
StyledText {
id: capabilityText
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.primary
}
}
}
}
}
}
}
StyledText {
anchors.centerIn: listArea
text: I18n.tr("No plugins found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: !root.isLoading && root.filteredPlugins.length === 0
}
}
}
}
}
DankModal {
id: thirdPartyConfirmModal
width: 500
height: 300
allowStacking: true
backgroundOpacity: 0.4
closeOnEscapeKey: true
content: Component {
FocusScope {
anchors.fill: parent
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
thirdPartyConfirmModal.close()
event.accepted = true
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "warning"
size: Theme.iconSize
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Third-Party Plugin Warning")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: I18n.tr("Third-party plugins are created by the community and are not officially supported by DankMaterialShell.\n\nThese plugins may pose security and privacy risks - install at your own risk.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: Text.WordWrap
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("• Plugins may contain bugs or security issues")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• Review code before installation when possible")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• Install only from trusted sources")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: parent.width
height: parent.height - parent.spacing * 3 - y
}
Row {
anchors.right: parent.right
spacing: Theme.spacingM
DankButton {
text: I18n.tr("Cancel")
iconName: "close"
onClicked: thirdPartyConfirmModal.close()
}
DankButton {
text: I18n.tr("I Understand")
iconName: "check"
onClicked: {
SessionData.setShowThirdPartyPlugins(true)
root.updateFilteredPlugins()
thirdPartyConfirmModal.close()
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,396 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
StyledRect {
id: root
property var pluginData: null
property string expandedPluginId: ""
property bool hasUpdate: false
property bool isReloading: false
property string pluginId: pluginData ? pluginData.id : ""
property string pluginDirectoryName: {
if (pluginData && pluginData.pluginDirectory) {
var path = pluginData.pluginDirectory
return path.substring(path.lastIndexOf('/') + 1)
}
return pluginId
}
property string pluginName: pluginData ? (pluginData.name || pluginData.id) : ""
property string pluginVersion: pluginData ? (pluginData.version || "1.0.0") : ""
property string pluginAuthor: pluginData ? (pluginData.author || "Unknown") : ""
property string pluginDescription: pluginData ? (pluginData.description || "") : ""
property string pluginIcon: pluginData ? (pluginData.icon || "extension") : "extension"
property string pluginSettingsPath: pluginData ? (pluginData.settingsPath || "") : ""
property var pluginPermissions: pluginData ? (pluginData.permissions || []) : []
property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== ""
property bool isExpanded: expandedPluginId === pluginId
width: parent.width
height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height
radius: Theme.cornerRadius
color: (pluginMouseArea.containsMouse || updateArea.containsMouse || uninstallArea.containsMouse || reloadArea.containsMouse) ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh)
border.width: 0
MouseArea {
id: pluginMouseArea
anchors.fill: parent
anchors.bottomMargin: root.isExpanded ? settingsContainer.height : 0
hoverEnabled: true
cursorShape: root.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.hasSettings
onClicked: {
root.expandedPluginId = root.expandedPluginId === root.pluginId ? "" : root.pluginId
}
}
Column {
id: pluginItemColumn
width: parent.width
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: root.pluginIcon
size: Theme.iconSize
color: PluginService.isPluginLoaded(root.pluginId) ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - toggleRow.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingXS
width: parent.width
StyledText {
text: root.pluginName
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
name: root.hasSettings ? (root.isExpanded ? "expand_less" : "expand_more") : ""
size: 16
color: root.hasSettings ? Theme.primary : "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.hasSettings
}
}
StyledText {
text: "v" + root.pluginVersion + " by " + root.pluginAuthor
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
Row {
id: toggleRow
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Rectangle {
width: 28
height: 28
radius: 14
color: updateArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
visible: DMSService.dmsAvailable && PluginService.isPluginLoaded(root.pluginId) && root.hasUpdate
DankIcon {
anchors.centerIn: parent
name: "download"
size: 16
color: updateArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: updateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const currentPluginName = root.pluginName
const currentPluginId = root.pluginId
DMSService.update(currentPluginName, response => {
if (response.error) {
ToastService.showError("Update failed: " + response.error)
} else {
ToastService.showInfo("Plugin updated: " + currentPluginName)
PluginService.forceRescanPlugin(currentPluginId)
if (DMSService.apiVersion >= 8) {
DMSService.listInstalled()
}
}
})
}
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item) {
const p = mapToItem(null, width / 2, 0)
tooltipLoader.item.show(I18n.tr("Update Plugin"), p.x, p.y - 40, null)
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: uninstallArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
visible: DMSService.dmsAvailable
DankIcon {
anchors.centerIn: parent
name: "delete"
size: 16
color: uninstallArea.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: uninstallArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const currentPluginName = root.pluginName
DMSService.uninstall(currentPluginName, response => {
if (response.error) {
ToastService.showError("Uninstall failed: " + response.error)
} else {
ToastService.showInfo("Plugin uninstalled: " + currentPluginName)
PluginService.scanPlugins()
if (root.isExpanded) {
root.expandedPluginId = ""
}
}
})
}
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item) {
const p = mapToItem(null, width / 2, 0)
tooltipLoader.item.show(I18n.tr("Uninstall Plugin"), p.x, p.y - 40, null)
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: reloadArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
visible: PluginService.isPluginLoaded(root.pluginId)
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 16
color: reloadArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: reloadArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const currentPluginId = root.pluginId
const currentPluginName = root.pluginName
root.isReloading = true
if (PluginService.reloadPlugin(currentPluginId)) {
ToastService.showInfo("Plugin reloaded: " + currentPluginName)
} else {
ToastService.showError("Failed to reload plugin: " + currentPluginName)
root.isReloading = false
}
}
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item) {
const p = mapToItem(null, width / 2, 0)
tooltipLoader.item.show(I18n.tr("Reload Plugin"), p.x, p.y - 40, null)
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
DankToggle {
id: pluginToggle
anchors.verticalCenter: parent.verticalCenter
checked: PluginService.isPluginLoaded(root.pluginId)
onToggled: isChecked => {
const currentPluginId = root.pluginId
const currentPluginName = root.pluginName
if (isChecked) {
if (PluginService.enablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin enabled: " + currentPluginName)
} else {
ToastService.showError("Failed to enable plugin: " + currentPluginName)
checked = false
}
} else {
if (PluginService.disablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin disabled: " + currentPluginName)
if (root.isExpanded) {
root.expandedPluginId = ""
}
} else {
ToastService.showError("Failed to disable plugin: " + currentPluginName)
checked = true
}
}
}
}
}
}
StyledText {
width: parent.width
text: root.pluginDescription
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
visible: root.pluginDescription !== ""
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: root.pluginPermissions && Array.isArray(root.pluginPermissions) && root.pluginPermissions.length > 0
Repeater {
model: root.pluginPermissions
Rectangle {
height: 20
width: permissionText.implicitWidth + Theme.spacingXS * 2
radius: 10
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 1
StyledText {
id: permissionText
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.primary
}
}
}
}
}
FocusScope {
id: settingsContainer
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: root.isExpanded && root.hasSettings ? (settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 0) : 0
clip: true
focus: root.isExpanded && root.hasSettings
Keys.onPressed: event => {
event.accepted = true
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHighest
radius: Theme.cornerRadius
anchors.topMargin: Theme.spacingXS
border.width: 0
}
Loader {
id: settingsLoader
anchors.fill: parent
anchors.margins: Theme.spacingL
active: root.isExpanded && root.hasSettings && PluginService.isPluginLoaded(root.pluginId)
asynchronous: false
source: {
if (active && root.pluginSettingsPath) {
var path = root.pluginSettingsPath
if (!path.startsWith("file://")) {
path = "file://" + path
}
return path
}
return ""
}
onLoaded: {
if (item && typeof PluginService !== "undefined") {
item.pluginService = PluginService
}
if (item && typeof PopoutService !== "undefined" && "popoutService" in item) {
item.popoutService = PopoutService
}
if (item) {
Qt.callLater(() => {
settingsContainer.focus = true
item.forceActiveFocus()
})
}
}
}
StyledText {
anchors.centerIn: parent
text: !PluginService.isPluginLoaded(root.pluginId) ?
"Enable plugin to access settings" :
(settingsLoader.status === Loader.Error ?
"Failed to load settings" :
"No configurable settings")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: root.isExpanded && (!settingsLoader.active || settingsLoader.status === Loader.Error)
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -65,17 +65,6 @@ Item {
checked) checked)
} }
} }
DankToggle {
width: parent.width
text: I18n.tr("Window Scrolling")
description: I18n.tr("Scroll through windows, rather than workspaces")
checked: SettingsData.workspaceScrolling
visible: CompositorService.isNiri
onToggled: checked => {
return SettingsData.setWorkspaceScrolling(checked)
}
}
DankToggle { DankToggle {
width: parent.width width: parent.width
text: I18n.tr("Workspace Padding") text: I18n.tr("Workspace Padding")
@@ -149,6 +138,17 @@ Item {
return SettingsData.setWorkspacesPerMonitor(checked); return SettingsData.setWorkspacesPerMonitor(checked);
} }
} }
DankToggle {
width: parent.width
text: I18n.tr("Show All Tags")
description: I18n.tr("Show all 9 tags instead of only occupied tags (DWL only)")
checked: SettingsData.dwlShowAllTags
visible: CompositorService.isDwl
onToggled: checked => {
return SettingsData.setDwlShowAllTags(checked);
}
}
} }
} }

View File

@@ -321,6 +321,7 @@ Column {
|| modelData.id === "music" || modelData.id === "music"
|| modelData.id === "focusedWindow" || modelData.id === "focusedWindow"
|| modelData.id === "runningApps" || modelData.id === "runningApps"
|| modelData.id === "keyboard_layout_name"
DankActionButton { DankActionButton {
id: smallSizeButton id: smallSizeButton
@@ -406,6 +407,7 @@ Column {
visible: modelData.id === "clock" visible: modelData.id === "clock"
|| modelData.id === "focusedWindow" || modelData.id === "focusedWindow"
|| modelData.id === "runningApps" || modelData.id === "runningApps"
|| modelData.id === "keyboard_layout_name"
iconName: { iconName: {
if (modelData.id === "clock") if (modelData.id === "clock")
return SettingsData.clockCompactMode ? "zoom_out" : "zoom_in" return SettingsData.clockCompactMode ? "zoom_out" : "zoom_in"
@@ -413,6 +415,8 @@ Column {
return SettingsData.focusedWindowCompactMode ? "zoom_out" : "zoom_in" return SettingsData.focusedWindowCompactMode ? "zoom_out" : "zoom_in"
if (modelData.id === "runningApps") if (modelData.id === "runningApps")
return SettingsData.runningAppsCompactMode ? "zoom_out" : "zoom_in" return SettingsData.runningAppsCompactMode ? "zoom_out" : "zoom_in"
if (modelData.id === "keyboard_layout_name")
return SettingsData.keyboardLayoutNameCompactMode ? "zoom_out" : "zoom_in"
return "zoom_in" return "zoom_in"
} }
iconSize: 16 iconSize: 16
@@ -423,6 +427,8 @@ Column {
return SettingsData.focusedWindowCompactMode ? Theme.primary : Theme.outline return SettingsData.focusedWindowCompactMode ? Theme.primary : Theme.outline
if (modelData.id === "runningApps") if (modelData.id === "runningApps")
return SettingsData.runningAppsCompactMode ? Theme.primary : Theme.outline return SettingsData.runningAppsCompactMode ? Theme.primary : Theme.outline
if (modelData.id === "keyboard_layout_name")
return SettingsData.keyboardLayoutNameCompactMode ? Theme.primary : Theme.outline
return Theme.outline return Theme.outline
} }
onClicked: { onClicked: {
@@ -438,6 +444,10 @@ Column {
root.compactModeChanged( root.compactModeChanged(
"runningApps", "runningApps",
!SettingsData.runningAppsCompactMode) !SettingsData.runningAppsCompactMode)
} else if (modelData.id === "keyboard_layout_name") {
root.compactModeChanged(
"keyboard_layout_name",
!SettingsData.keyboardLayoutNameCompactMode)
} }
} }
onEntered: { onEntered: {
@@ -450,6 +460,8 @@ Column {
tooltipText = SettingsData.focusedWindowCompactMode ? "Full Size" : "Compact" tooltipText = SettingsData.focusedWindowCompactMode ? "Full Size" : "Compact"
} else if (modelData.id === "runningApps") { } else if (modelData.id === "runningApps") {
tooltipText = SettingsData.runningAppsCompactMode ? "Full Size" : "Compact" tooltipText = SettingsData.runningAppsCompactMode ? "Full Size" : "Compact"
} else if (modelData.id === "keyboard_layout_name") {
tooltipText = SettingsData.keyboardLayoutNameCompactMode ? "Full Size" : "Compact"
} }
const p = compactModeButton.mapToItem(null, compactModeButton.width / 2, 0) const p = compactModeButton.mapToItem(null, compactModeButton.width / 2, 0)
compactTooltipLoader.item.show(tooltipText, p.x, p.y - 40, null) compactTooltipLoader.item.show(tooltipText, p.x, p.y - 40, null)

View File

@@ -476,7 +476,7 @@ Variants {
visible: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview visible: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview
blurEnabled: true blurEnabled: true
blur: 0.8 blur: 0.8
blurMax: 48 blurMax: 75
} }
} }
} }

View File

@@ -12,7 +12,7 @@
</div> </div>
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/). Optimized for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors. A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/). Optimized for the [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [sway](https://swaywm.org/), and [dwl/mangowc](https://github.com/DreamMaoMao/mangowc) compositors.
Features notifications, app launcher, wallpaper customization, and fully customizable with [plugins](https://github.com/AvengeMedia/dms-plugin-registry). Features notifications, app launcher, wallpaper customization, and fully customizable with [plugins](https://github.com/AvengeMedia/dms-plugin-registry).
@@ -101,7 +101,7 @@ curl -fsSL https://install.danklinux.com | sh
- **Control Center** High-level view of network, bluetooth, and audio status - **Control Center** High-level view of network, bluetooth, and audio status
- **Privacy Indicator** Attempts to reveal if a microphone or screen recording session is active, relying on Pipewire data sources - **Privacy Indicator** Attempts to reveal if a microphone or screen recording session is active, relying on Pipewire data sources
- **Idle Inhibitor** Creates a systemd idle inhibitor to prevent sleep/locking from occuring. - **Idle Inhibitor** Creates a systemd idle inhibitor to prevent sleep/locking from occuring.
- **Spotlight Launcher** A central app launcher/search that can be triggered via an IPC keybinding. - **Spotlight Launcher** A central search/launcher - apps, files, emojis, running apps, calculator, command running - and basically anything since it can be enriched with plugins.
- **Central Command** A combined music, weather, calendar, and events PopUp. - **Central Command** A combined music, weather, calendar, and events PopUp.
- **Process List** A process list, with system metrics and information. More detailed modal available via IPC. - **Process List** A process list, with system metrics and information. More detailed modal available via IPC.
- **Notification Center** A center for notifications that has support for grouping. - **Notification Center** A center for notifications that has support for grouping.
@@ -134,7 +134,7 @@ curl -fsSL https://install.danklinux.com | sh
### Compositor Setup ### Compositor Setup
DankMaterialShell particularly aims at supporting the **niri** and **Hyprland** compositors, but it does support more wayland compositors with a diminished feature set (no monitor off, workspace switcher, overview integration, etc.): DankMaterialShell particularly aims at supporting the **niri**, **Hyprland**, **sway**, and **dwl/MangoWC** compositors, but it does support more wayland compositors with a diminished feature set (no monitor off, workspace switcher, overview integration, etc.):
**Niri**: **Niri**:
```bash ```bash
@@ -164,6 +164,10 @@ sudo dnf copr enable solopasha/hyprland && sudo dnf install hyprland
For detailed Hyprland installation instructions, see the [Hyprland wiki](https://wiki.hypr.land/Getting-Started/Installation/). For detailed Hyprland installation instructions, see the [Hyprland wiki](https://wiki.hypr.land/Getting-Started/Installation/).
**sway/dwl (MangoWC)**:
TODO - not documented.
### Dank Shell Installation ### Dank Shell Installation
*feel free to contribute steps for other distributions* *feel free to contribute steps for other distributions*
@@ -267,31 +271,9 @@ sudo dnf copr enable avengemedia/danklinux && sudo dnf install quickshell-git
# ! TODO - document other distros # ! TODO - document other distros
``` ```
#### 2. Install fonts #### 2. Install the shell
*Inter Variable* and *Fira Code* are not strictly required, but they are the default fonts of dms.
#### 2.1 Install Material Symbols #### 2.1. Clone latest QML
```bash
sudo curl -L "https://github.com/google/material-design-icons/raw/master/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.ttf" -o /usr/share/fonts/MaterialSymbolsRounded.ttf
```
#### 2.2 Install Inter Variable
```bash
sudo curl -L "https://github.com/rsms/inter/raw/refs/tags/v4.1/docs/font-files/InterVariable.ttf" -o /usr/share/fonts/InterVariable.ttf
```
#### 2.3 Install Fira Code (monospace font)
```bash
sudo curl -L "https://github.com/tonsky/FiraCode/releases/latest/download/FiraCode-Regular.ttf" -o /usr/share/fonts/FiraCode-Regular.ttf
```
#### 2.4 Refresh font cache
```bash
fc-cache -fv
```
#### 3. Install the shell
#### 3.1. Clone latest QML
```bash ```bash
mkdir ~/.config/quickshell && git clone https://github.com/AvengeMedia/DankMaterialShell.git ~/.config/quickshell/dms mkdir ~/.config/quickshell && git clone https://github.com/AvengeMedia/DankMaterialShell.git ~/.config/quickshell/dms
@@ -305,7 +287,7 @@ cd ~/.config/quickshell/dms
git checkout $(git describe --tags --abbrev=0) git checkout $(git describe --tags --abbrev=0)
``` ```
#### 3.2. Install latest dms CLI #### 2.2. Install latest dms CLI
```bash ```bash
sudo sh -c "curl -L https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').gz | gunzip | tee /usr/local/bin/dms > /dev/null && chmod +x /usr/local/bin/dms" sudo sh -c "curl -L https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').gz | gunzip | tee /usr/local/bin/dms > /dev/null && chmod +x /usr/local/bin/dms"
@@ -319,9 +301,9 @@ git clone https://github.com/AvengeMedia/danklinux.git && cd danklinux
make && sudo make install make && sudo make install
``` ```
#### 4. Optional Features (system monitoring, clipboard history, brightness controls, etc.) #### 3. Optional Features (system monitoring, clipboard history, brightness controls, etc.)
#### 4.1 Core optional dependencies #### 3.1 Core optional dependencies
```bash ```bash
# Arch Linux # Arch Linux
sudo pacman -S cava wl-clipboard cliphist brightnessctl qt6-multimedia accountsservice sudo pacman -S cava wl-clipboard cliphist brightnessctl qt6-multimedia accountsservice
@@ -329,13 +311,13 @@ paru -S matugen-bin dgop
# Fedora # Fedora
sudo dnf install cava wl-clipboard brightnessctl qt6-qtmultimedia accountsservice sudo dnf install cava wl-clipboard brightnessctl qt6-qtmultimedia accountsservice
sudo dnf copr enable avengemedia/danklinux && sudo dnf install cliphist ghostty hyprpicker material-symbols-fonts matugen sudo dnf copr enable avengemedia/danklinux && sudo dnf install cliphist ghostty hyprpicker matugen
``` ```
Note: by enabling and installing the avengemedia/dms copr above, these core dependencies will automatically be available for use. Note: by enabling and installing the avengemedia/dms copr above, these core dependencies will automatically be available for use.
*Other distros will just need to find sources for the above packages* *Other distros will just need to find sources for the above packages*
#### 4.2 - dgop manual installation #### 3.2 - dgop manual installation
`dgop` is available via AUR and a nix flake, other distributions can install it manually. `dgop` is available via AUR and a nix flake, other distributions can install it manually.
@@ -460,15 +442,9 @@ layer-rule {
If using "Blur Layer" option, you may want the blurred layer to appear on overview only, that can be done with some layer rules: If using "Blur Layer" option, you may want the blurred layer to appear on overview only, that can be done with some layer rules:
```kdl ```kdl
layer-rule {
match namespace="dms:blurwallpaper"
opacity 0.0
}
layer-rule { layer-rule {
match namespace="dms:blurwallpaper" match namespace="dms:blurwallpaper"
place-within-backdrop true place-within-backdrop true
opacity 1.0
} }
``` ```
@@ -821,7 +797,6 @@ All settings are configurable in
**Common issues:** **Common issues:**
- **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material`
- **No dynamic theming:** Install matugen and enable in settings - **No dynamic theming:** Install matugen and enable in settings
- **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME - **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME
- **Calendar not syncing:** Check vdirsyncer credentials and network connectivity - **Calendar not syncing:** Check vdirsyncer credentials and network connectivity

View File

@@ -2,6 +2,7 @@ pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
@@ -68,8 +69,8 @@ Singleton {
function scanSoundThemes() { function scanSoundThemes() {
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS")
const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== "" const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== ""
? xdgDataDirs.split(":") ? xdgDataDirs.split(":").concat(Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation)))
: ["/usr/share", "/usr/local/share", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share"] : ["/usr/share", "/usr/local/share", Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))]
const basePaths = searchPaths.map(p => p + "/sounds").join(" ") const basePaths = searchPaths.map(p => p + "/sounds").join(" ")
const script = ` const script = `
@@ -134,8 +135,8 @@ Singleton {
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS")
const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== "" const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== ""
? xdgDataDirs.split(":") ? xdgDataDirs.split(":").concat(Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation)))
: ["/usr/share", "/usr/local/share", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share"] : ["/usr/share", "/usr/local/share", Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))]
const extensions = ["oga", "ogg", "wav", "mp3", "flac"] const extensions = ["oga", "ogg", "wav", "mp3", "flac"]
const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName

View File

@@ -223,6 +223,13 @@ Singleton {
let eventId = event.title + "_" + event['start-date'] let eventId = event.title + "_" + event['start-date']
+ "_" + (event['start-time'] || 'allday') + "_" + (event['start-time'] || 'allday')
// Create event object template // Create event object template
let extractedUrl = ""
if (!event.url && event.description) {
let urlMatch = event.description.match(/https?:\/\/[^\s]+/)
if (urlMatch) {
extractedUrl = urlMatch[0]
}
}
let eventTemplate = { let eventTemplate = {
"id": eventId, "id": eventId,
"title": event.title || "Untitled Event", "title": event.title || "Untitled Event",
@@ -230,7 +237,7 @@ Singleton {
"end": endTime, "end": endTime,
"location": event.location || "", "location": event.location || "",
"description": event.description || "", "description": event.description || "",
"url": event.url || "", "url": event.url || extractedUrl,
"calendar": "", "calendar": "",
"color": "", "color": "",
"allDay": event['all-day'] === "True", "allDay": event['all-day'] === "True",

View File

@@ -12,10 +12,13 @@ Singleton {
property bool isHyprland: false property bool isHyprland: false
property bool isNiri: false property bool isNiri: false
property bool isDwl: false
property bool isSway: false
property string compositor: "unknown" property string compositor: "unknown"
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
readonly property string swaySocket: Quickshell.env("SWAYSOCK")
property bool useNiriSorting: isNiri && NiriService property bool useNiriSorting: isNiri && NiriService
property var sortedToplevels: sortedToplevelsCache property var sortedToplevels: sortedToplevelsCache
@@ -26,6 +29,30 @@ Singleton {
property bool _hasRefreshedOnce: false property bool _hasRefreshedOnce: false
property var _coordCache: ({}) property var _coordCache: ({})
property int _refreshCount: 0
property real _refreshWindowStart: 0
readonly property int _maxRefreshesPerSecond: 3
function getScreenScale(screen) {
if (!screen) return 1
if (isNiri && screen) {
const niriScale = NiriService.displayScales[screen.name]
if (niriScale !== undefined) return niriScale
}
if (isHyprland && screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
if (isDwl && screen) {
const dwlScale = DwlService.getOutputScale(screen.name)
if (dwlScale !== undefined && dwlScale > 0) return dwlScale
}
return screen?.devicePixelRatio || 1
}
Timer { Timer {
id: refreshTimer id: refreshTimer
@@ -53,6 +80,19 @@ Singleton {
function scheduleRefresh() { function scheduleRefresh() {
if (!isHyprland) return if (!isHyprland) return
if (_refreshScheduled) return if (_refreshScheduled) return
const now = Date.now()
if (now - _refreshWindowStart > 1000) {
_refreshCount = 0
_refreshWindowStart = now
}
if (_refreshCount >= _maxRefreshesPerSecond) {
console.warn("CompositorService: Refresh rate limit exceeded, skipping refresh")
return
}
_refreshCount++
_refreshScheduled = true _refreshScheduled = true
refreshTimer.restart() refreshTimer.restart()
} }
@@ -87,6 +127,15 @@ Singleton {
Qt.callLater(() => NiriService.generateNiriLayoutConfig()) Qt.callLater(() => NiriService.generateNiriLayoutConfig())
} }
Connections {
target: DwlService
function onStateChanged() {
if (isDwl && !isHyprland && !isNiri) {
scheduleSort()
}
}
}
function computeSortedToplevels() { function computeSortedToplevels() {
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
return [] return []
@@ -125,6 +174,18 @@ Singleton {
} catch(e) { return fb } } catch(e) { return fb }
} }
let currentAddresses = new Set()
for (let i = 0; i < items.length; i++) {
const addr = items[i]?.address
if (addr) currentAddresses.add(addr)
}
for (let cachedAddr in _coordCache) {
if (!currentAddresses.has(cachedAddr)) {
delete _coordCache[cachedAddr]
}
}
let snap = [] let snap = []
let missingAnyPosition = false let missingAnyPosition = false
let hasNewWindow = false let hasNewWindow = false
@@ -331,6 +392,8 @@ Singleton {
if (hyprlandSignature && hyprlandSignature.length > 0) { if (hyprlandSignature && hyprlandSignature.length > 0) {
isHyprland = true isHyprland = true
isNiri = false isNiri = false
isDwl = false
isSway = false
compositor = "hyprland" compositor = "hyprland"
console.info("CompositorService: Detected Hyprland") console.info("CompositorService: Detected Hyprland")
try { try {
@@ -344,33 +407,103 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
isNiri = true isNiri = true
isHyprland = false isHyprland = false
isDwl = false
isSway = false
compositor = "niri" compositor = "niri"
console.info("CompositorService: Detected Niri with socket:", niriSocket) console.info("CompositorService: Detected Niri with socket:", niriSocket)
NiriService.generateNiriBinds() NiriService.generateNiriBinds()
} else { NiriService.generateNiriBlurrule()
isHyprland = false
isNiri = true
compositor = "niri"
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
} }
}, 0) }, 0)
return
}
if (swaySocket && swaySocket.length > 0) {
Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false
isHyprland = false
isDwl = false
isSway = true
compositor = "sway"
console.info("CompositorService: Detected Sway with socket:", swaySocket)
}
}, 0)
return
}
if (DMSService.dmsAvailable) {
Qt.callLater(checkForDwl)
} else { } else {
isHyprland = false isHyprland = false
isNiri = false isNiri = false
isDwl = false
isSway = false
compositor = "unknown" compositor = "unknown"
console.warn("CompositorService: No compositor detected") console.warn("CompositorService: No compositor detected")
} }
} }
Connections {
target: DMSService
function onCapabilitiesReceived() {
if (!isHyprland && !isNiri && !isDwl) {
checkForDwl()
}
}
}
function checkForDwl() {
if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) {
isHyprland = false
isNiri = false
isDwl = true
compositor = "dwl"
console.info("CompositorService: Detected DWL via DMS capability")
}
}
function powerOffMonitors() { function powerOffMonitors() {
if (isNiri) return NiriService.powerOffMonitors() if (isNiri) return NiriService.powerOffMonitors()
if (isHyprland) return Hyprland.dispatch("dpms off") if (isHyprland) return Hyprland.dispatch("dpms off")
if (isDwl) return _dwlPowerOffMonitors()
if (isSway) { try { I3.dispatch("output * dpms off") } catch(_){} return }
console.warn("CompositorService: Cannot power off monitors, unknown compositor") console.warn("CompositorService: Cannot power off monitors, unknown compositor")
} }
function powerOnMonitors() { function powerOnMonitors() {
if (isNiri) return NiriService.powerOnMonitors() if (isNiri) return NiriService.powerOnMonitors()
if (isHyprland) return Hyprland.dispatch("dpms on") if (isHyprland) return Hyprland.dispatch("dpms on")
if (isDwl) return _dwlPowerOnMonitors()
if (isSway) { try { I3.dispatch("output * dpms on") } catch(_){} return }
console.warn("CompositorService: Cannot power on monitors, unknown compositor") console.warn("CompositorService: Cannot power on monitors, unknown compositor")
} }
function _dwlPowerOffMonitors() {
if (!Quickshell.screens || Quickshell.screens.length === 0) {
console.warn("CompositorService: No screens available for DWL power off")
return
}
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i]
if (screen && screen.name) {
Quickshell.execDetached(["mmsg", "-d", "disable_monitor," + screen.name])
}
}
}
function _dwlPowerOnMonitors() {
if (!Quickshell.screens || Quickshell.screens.length === 0) {
console.warn("CompositorService: No screens available for DWL power on")
return
}
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i]
if (screen && screen.name) {
Quickshell.execDetached(["mmsg", "-d", "enable_monitor," + screen.name])
}
}
}
} }

View File

@@ -37,7 +37,7 @@ Singleton {
property var savedConnections: [] property var savedConnections: []
property var ssidToConnectionName: ({}) property var ssidToConnectionName: ({})
property var wifiSignalIcon: { property var wifiSignalIcon: {
if (!wifiConnected || networkStatus !== "wifi") { if (!wifiConnected) {
return "wifi_off" return "wifi_off"
} }
if (wifiSignalStrength >= 50) { if (wifiSignalStrength >= 50) {
@@ -73,6 +73,9 @@ Singleton {
property var vpnActive: [] property var vpnActive: []
property bool vpnAvailable: false property bool vpnAvailable: false
property bool vpnIsBusy: false property bool vpnIsBusy: false
property string lastConnectedVpnUuid: ""
property string pendingVpnUuid: ""
property var vpnBusyStartTime: 0
property alias profiles: root.vpnProfiles property alias profiles: root.vpnProfiles
property alias activeConnections: root.vpnActive property alias activeConnections: root.vpnActive
@@ -118,6 +121,7 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
root.userPreference = SettingsData.networkPreference root.userPreference = SettingsData.networkPreference
lastConnectedVpnUuid = SettingsData.vpnLastConnected || ""
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
checkDMSCapabilities() checkDMSCapabilities()
} }
@@ -271,8 +275,41 @@ Singleton {
vpnProfiles = state.vpnProfiles vpnProfiles = state.vpnProfiles
} }
const previousVpnActive = vpnActive
vpnActive = state.vpnActive || [] vpnActive = state.vpnActive || []
if (vpnConnected && activeUuid) {
lastConnectedVpnUuid = activeUuid
SettingsData.setVpnLastConnected(activeUuid)
}
if (vpnIsBusy) {
const busyDuration = Date.now() - vpnBusyStartTime
const timeout = 30000
if (busyDuration > timeout) {
console.warn("DMSNetworkService: VPN operation timed out after", timeout, "ms")
vpnIsBusy = false
pendingVpnUuid = ""
vpnBusyStartTime = 0
} else if (pendingVpnUuid) {
const isPendingVpnActive = activeUuids.includes(pendingVpnUuid)
if (isPendingVpnActive) {
vpnIsBusy = false
pendingVpnUuid = ""
vpnBusyStartTime = 0
}
} else {
const previousCount = previousVpnActive ? previousVpnActive.length : 0
const currentCount = vpnActive ? vpnActive.length : 0
if (previousCount !== currentCount) {
vpnIsBusy = false
vpnBusyStartTime = 0
}
}
}
userPreference = state.preference || "auto" userPreference = state.preference || "auto"
isConnecting = state.isConnecting || false isConnecting = state.isConnecting || false
connectingSSID = state.connectingSSID || "" connectingSSID = state.connectingSSID || ""
@@ -730,6 +767,8 @@ Singleton {
if (!vpnAvailable || vpnIsBusy) return if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true vpnIsBusy = true
pendingVpnUuid = uuidOrName
vpnBusyStartTime = Date.now()
const params = { const params = {
uuidOrName: uuidOrName, uuidOrName: uuidOrName,
@@ -737,12 +776,11 @@ Singleton {
} }
DMSService.sendRequest("network.vpn.connect", params, response => { DMSService.sendRequest("network.vpn.connect", params, response => {
vpnIsBusy = false
if (response.error) { if (response.error) {
vpnIsBusy = false
pendingVpnUuid = ""
vpnBusyStartTime = 0
ToastService.showError(I18n.tr("Failed to connect VPN")) ToastService.showError(I18n.tr("Failed to connect VPN"))
} else {
Qt.callLater(() => getState())
} }
}) })
} }
@@ -755,18 +793,18 @@ Singleton {
if (!vpnAvailable || vpnIsBusy) return if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true vpnIsBusy = true
pendingVpnUuid = ""
vpnBusyStartTime = Date.now()
const params = { const params = {
uuidOrName: uuidOrName uuidOrName: uuidOrName
} }
DMSService.sendRequest("network.vpn.disconnect", params, response => { DMSService.sendRequest("network.vpn.disconnect", params, response => {
vpnIsBusy = false
if (response.error) { if (response.error) {
vpnIsBusy = false
vpnBusyStartTime = 0
ToastService.showError(I18n.tr("Failed to disconnect VPN")) ToastService.showError(I18n.tr("Failed to disconnect VPN"))
} else {
Qt.callLater(() => getState())
} }
}) })
} }
@@ -779,14 +817,12 @@ Singleton {
if (!vpnAvailable || vpnIsBusy) return if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true vpnIsBusy = true
pendingVpnUuid = ""
DMSService.sendRequest("network.vpn.disconnectAll", null, response => { DMSService.sendRequest("network.vpn.disconnectAll", null, response => {
vpnIsBusy = false
if (response.error) { if (response.error) {
vpnIsBusy = false
ToastService.showError(I18n.tr("Failed to disconnect VPNs")) ToastService.showError(I18n.tr("Failed to disconnect VPNs"))
} else {
Qt.callLater(() => getState())
} }
}) })
} }
@@ -805,8 +841,14 @@ Singleton {
return return
} }
if (vpnProfiles.length > 0) { if (vpnConnected) {
connectVpn(vpnProfiles[0].uuid) disconnectAllVpns()
return
}
const targetUuid = lastConnectedVpnUuid || (vpnProfiles.length > 0 ? vpnProfiles[0].uuid : "")
if (targetUuid) {
connectVpn(targetUuid)
} }
} }

View File

@@ -42,6 +42,7 @@ Singleton {
signal capabilitiesReceived() signal capabilitiesReceived()
signal credentialsRequest(var data) signal credentialsRequest(var data)
signal bluetoothPairingRequest(var data) signal bluetoothPairingRequest(var data)
signal dwlStateUpdate(var data)
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -266,6 +267,8 @@ Singleton {
} }
} else if (service === "bluetooth.pairing") { } else if (service === "bluetooth.pairing") {
bluetoothPairingRequest(data) bluetoothPairingRequest(data)
} else if (service === "dwl") {
dwlStateUpdate(data)
} }
} }

143
Services/DSearchService.qml Normal file
View File

@@ -0,0 +1,143 @@
pragma Singleton
pragma ComponentBehavior
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property bool dsearchAvailable: false
property int searchIdCounter: 0
signal searchResultsReceived(var results)
signal statsReceived(var stats)
signal errorOccurred(string error)
Process {
id: checkProcess
command: ["sh", "-c", "command -v dsearch"]
running: true
stdout: SplitParser {
onRead: line => {
if (line && line.trim().length > 0) {
root.dsearchAvailable = true
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.dsearchAvailable = false
}
}
}
function ping(callback) {
if (!dsearchAvailable) {
if (callback) {
callback({ "error": "dsearch not available" })
}
return
}
Proc.runCommand("dsearch-ping", ["dsearch", "ping", "--json"], (stdout, exitCode) => {
if (callback) {
if (exitCode === 0) {
try {
const response = JSON.parse(stdout)
callback({ "result": response })
} catch (e) {
callback({ "error": "failed to parse ping response" })
}
} else {
callback({ "error": "ping failed" })
}
}
})
}
function search(query, params, callback) {
if (!query || query.length === 0) {
if (callback) {
callback({ "error": "query is required" })
}
return
}
if (!dsearchAvailable) {
if (callback) {
callback({ "error": "dsearch not available" })
}
return
}
const args = ["dsearch", "search", query, "--json"]
if (params) {
if (params.limit !== undefined) {
args.push("-n", String(params.limit))
}
if (params.ext) {
args.push("-e", params.ext)
}
if (params.field) {
args.push("-f", params.field)
}
if (params.fuzzy) {
args.push("--fuzzy")
}
if (params.sort) {
args.push("--sort", params.sort)
}
if (params.desc !== undefined) {
args.push("--desc=" + (params.desc ? "true" : "false"))
}
if (params.minSize !== undefined) {
args.push("--min-size", String(params.minSize))
}
if (params.maxSize !== undefined) {
args.push("--max-size", String(params.maxSize))
}
}
Proc.runCommand("dsearch-search", args, (stdout, exitCode) => {
if (exitCode === 0) {
try {
const response = JSON.parse(stdout)
searchResultsReceived(response)
if (callback) {
callback({ "result": response })
}
} catch (e) {
const error = "failed to parse search response"
errorOccurred(error)
if (callback) {
callback({ "error": error })
}
}
} else if (exitCode === 124) {
const error = "search timed out"
errorOccurred(error)
if (callback) {
callback({ "error": error })
}
} else {
const error = "search failed"
errorOccurred(error)
if (callback) {
callback({ "error": error })
}
}
}, 100, 5000)
}
function rediscover() {
checkProcess.running = true
}
}

259
Services/DwlService.qml Normal file
View File

@@ -0,0 +1,259 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property bool dwlAvailable: false
property var outputs: ({})
property var tagCount: 9
property var layouts: []
property string activeOutput: ""
property var outputScales: ({})
signal stateChanged()
Connections {
target: DMSService
function onCapabilitiesReceived() {
checkCapabilities()
}
function onConnectionStateChanged() {
if (DMSService.isConnected) {
checkCapabilities()
} else {
dwlAvailable = false
}
}
function onDwlStateUpdate(data) {
if (dwlAvailable) {
handleStateUpdate(data)
}
}
}
Component.onCompleted: {
if (DMSService.dmsAvailable) {
checkCapabilities()
}
if (dwlAvailable) {
refreshOutputScales()
}
}
function checkCapabilities() {
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
dwlAvailable = false
return
}
const hasDwl = DMSService.capabilities.includes("dwl")
if (hasDwl && !dwlAvailable) {
dwlAvailable = true
console.info("DwlService: DWL capability detected")
requestState()
refreshOutputScales()
} else if (!hasDwl) {
dwlAvailable = false
}
}
function requestState() {
if (!DMSService.isConnected || !dwlAvailable) {
return
}
DMSService.sendRequest("dwl.getState", null, response => {
if (response.result) {
handleStateUpdate(response.result)
}
})
}
function handleStateUpdate(state) {
outputs = state.outputs || {}
tagCount = state.tagCount || 9
layouts = state.layouts || []
activeOutput = state.activeOutput || ""
stateChanged()
}
function setTags(outputName, tagmask, toggleTagset) {
if (!DMSService.isConnected || !dwlAvailable) {
return
}
DMSService.sendRequest("dwl.setTags", {
"output": outputName,
"tagmask": tagmask,
"toggleTagset": toggleTagset
}, response => {
if (response.error) {
console.warn("DwlService: setTags error:", response.error)
}
})
}
function setClientTags(outputName, andTags, xorTags) {
if (!DMSService.isConnected || !dwlAvailable) {
return
}
DMSService.sendRequest("dwl.setClientTags", {
"output": outputName,
"andTags": andTags,
"xorTags": xorTags
}, response => {
if (response.error) {
console.warn("DwlService: setClientTags error:", response.error)
}
})
}
function setLayout(outputName, index) {
if (!DMSService.isConnected || !dwlAvailable) {
return
}
DMSService.sendRequest("dwl.setLayout", {
"output": outputName,
"index": index
}, response => {
if (response.error) {
console.warn("DwlService: setLayout error:", response.error)
}
})
}
function getOutputState(outputName) {
if (!outputs || !outputs[outputName]) {
return null
}
return outputs[outputName]
}
function getActiveTags(outputName) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
return []
}
return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag)
}
function getTagsWithClients(outputName) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
return []
}
return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag)
}
function getUrgentTags(outputName) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
return []
}
return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag)
}
function switchToTag(outputName, tagIndex) {
const tagmask = 1 << tagIndex
setTags(outputName, tagmask, 0)
}
function toggleTag(outputName, tagIndex) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
console.log("toggleTag: no output or tags for", outputName)
return
}
let currentMask = 0
output.tags.forEach(tag => {
if (tag.state === 1) {
currentMask |= (1 << tag.tag)
}
})
const clickedMask = 1 << tagIndex
const newMask = currentMask ^ clickedMask
console.log("toggleTag:", outputName, "tag:", tagIndex, "currentMask:", currentMask.toString(2), "clickedMask:", clickedMask.toString(2), "newMask:", newMask.toString(2))
if (newMask === 0) {
console.log("toggleTag: newMask is 0, switching to tag", tagIndex)
setTags(outputName, 1 << tagIndex, 0)
} else {
console.log("toggleTag: setting combined mask", newMask)
setTags(outputName, newMask, 0)
}
}
function quit() {
Quickshell.execDetached(["mmsg", "-d", "quit"])
}
Process {
id: scaleQueryProcess
command: ["mmsg", "-A"]
running: false
stdout: StdioCollector {
onStreamFinished: {
try {
const newScales = {}
const lines = text.trim().split('\n')
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 3 && parts[1] === "scale_factor") {
const outputName = parts[0]
const scale = parseFloat(parts[2])
if (!isNaN(scale)) {
newScales[outputName] = scale
}
}
}
outputScales = newScales
} catch (e) {
console.warn("DwlService: Failed to parse mmsg output:", e)
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("DwlService: mmsg failed with exit code:", exitCode)
}
}
}
function refreshOutputScales() {
if (!dwlAvailable) return
scaleQueryProcess.running = true
}
function getOutputScale(outputName) {
return outputScales[outputName]
}
function getVisibleTags(outputName) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
return [0]
}
const visibleTags = new Set([0])
output.tags.forEach(tag => {
if (tag.state === 1 || tag.clients > 0) {
visibleTags.add(tag.tag)
}
})
return Array.from(visibleTags).sort((a, b) => a - b)
}
}

View File

@@ -19,7 +19,7 @@ Singleton {
Process { Process {
id: getKeybinds id: getKeybinds
running: true running: false
command: [root.scriptPath, "--path", root.hyprConfigPath] command: [root.scriptPath, "--path", root.hyprConfigPath]
stdout: SplitParser { stdout: SplitParser {
@@ -31,9 +31,22 @@ Singleton {
} }
} }
} }
onExited: (code) => {
if (code !== 0) {
console.warn("[HyprKeybindsService] Process exited with code:", code)
}
}
}
Component.onCompleted: {
getKeybinds.running = true
} }
function reload() { function reload() {
getKeybinds.running = false
Qt.callLater(function() {
getKeybinds.running = true getKeybinds.running = true
})
} }
} }

View File

@@ -10,7 +10,6 @@ import qs.Common
Singleton { Singleton {
id: root id: root
property int refCount: 0
property bool isActive: false property bool isActive: false
property string networkStatus: "disconnected" property string networkStatus: "disconnected"
property string primaryConnection: "" property string primaryConnection: ""
@@ -56,8 +55,6 @@ Singleton {
property string connectionError: "" property string connectionError: ""
property bool isScanning: false property bool isScanning: false
property bool autoScan: false
property bool wifiAvailable: true property bool wifiAvailable: true
property bool wifiToggling: false property bool wifiToggling: false
property bool changingPreference: false property bool changingPreference: false
@@ -66,7 +63,6 @@ Singleton {
property string connectionStatus: "" property string connectionStatus: ""
property string lastConnectionError: "" property string lastConnectionError: ""
property bool passwordDialogShouldReopen: false property bool passwordDialogShouldReopen: false
property bool autoRefreshEnabled: false
property string wifiPassword: "" property string wifiPassword: ""
property string forgetSSID: "" property string forgetSSID: ""
@@ -109,114 +105,20 @@ Singleton {
root.userPreference = SettingsData.networkPreference root.userPreference = SettingsData.networkPreference
} }
Component.onDestruction: {
nmStateMonitor.running = false
}
function activate() { function activate() {
if (!isActive) { if (!isActive) {
isActive = true isActive = true
console.info("LegacyNetworkService: Activating...") console.info("LegacyNetworkService: Activating...")
initializeDBusMonitors()
}
}
function addRef() {
refCount++
if (refCount === 1) {
startAutoScan()
}
}
function removeRef() {
refCount = Math.max(0, refCount - 1)
if (refCount === 0) {
stopAutoScan()
}
}
function initializeDBusMonitors() {
nmStateMonitor.running = true
doRefreshNetworkState() doRefreshNetworkState()
} }
Process {
id: nmStateMonitor
command: lowPriorityCmd.concat(["gdbus", "monitor", "--system", "--dest", "org.freedesktop.NetworkManager"])
running: false
property var lastRefreshTime: 0
property int minRefreshInterval: 1000
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
const now = Date.now()
if (line.includes("PropertiesChanged") && line.includes("org.freedesktop.NetworkManager.AccessPoint")) {
if (line.includes("'Strength'") && root.activeAccessPointPath && line.includes(root.activeAccessPointPath)) {
parseSignalStrengthFromDbus(line)
}
return
} }
if (line.includes("StateChanged") ||
line.includes("PrimaryConnectionChanged") ||
line.includes("WirelessEnabled") ||
(line.includes("ActiveConnection") && line.includes("State"))) {
if (now - nmStateMonitor.lastRefreshTime > nmStateMonitor.minRefreshInterval) {
nmStateMonitor.lastRefreshTime = now
refreshNetworkState()
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0 && !restartTimer.running) {
console.warn("NetworkManager monitor failed, restarting in 5s")
restartTimer.start()
}
}
}
Timer {
id: restartTimer
interval: 5000
running: false
onTriggered: nmStateMonitor.running = true
}
Timer {
id: refreshDebounceTimer
interval: 100
running: false
onTriggered: doRefreshNetworkState()
}
function refreshNetworkState() {
refreshDebounceTimer.restart()
}
function parseSignalStrengthFromDbus(line) {
const strengthMatch = line.match(/'Strength': <byte (0x[0-9a-fA-F]+)>/)
if (strengthMatch) {
const hexValue = strengthMatch[1]
const strength = parseInt(hexValue, 16)
if (strength >= 0 && strength <= 100) {
root.wifiSignalStrength = strength
}
}
}
function doRefreshNetworkState() { function doRefreshNetworkState() {
updatePrimaryConnection() updatePrimaryConnection()
updateDeviceStates() updateDeviceStates()
updateActiveConnections() updateActiveConnections()
updateWifiState() updateWifiState()
if (root.refCount > 0 && root.wifiEnabled) {
scanWifiNetworks()
}
} }
function updatePrimaryConnection() { function updatePrimaryConnection() {
@@ -333,9 +235,6 @@ Singleton {
getEthernetIP.running = true getEthernetIP.running = true
} else { } else {
root.ethernetIP = "" root.ethernetIP = ""
if (root.networkStatus === "ethernet") {
updatePrimaryConnection()
}
} }
} }
} }
@@ -907,18 +806,6 @@ Singleton {
} }
} }
function startAutoScan() {
root.autoScan = true
root.autoRefreshEnabled = true
if (root.wifiEnabled) {
scanWifi()
}
}
function stopAutoScan() {
root.autoScan = false
root.autoRefreshEnabled = false
}
function fetchNetworkInfo(ssid) { function fetchNetworkInfo(ssid) {
root.networkInfoSSID = ssid root.networkInfoSSID = ssid

View File

@@ -39,6 +39,11 @@ Singleton {
signal windowUrgentChanged signal windowUrgentChanged
function setWorkspaces(newMap) {
root.workspaces = newMap
allWorkspaces = Object.values(newMap).sort((a, b) => a.idx - b.idx)
}
Component.onCompleted: fetchOutputs() Component.onCompleted: fetchOutputs()
Timer { Timer {
@@ -280,8 +285,7 @@ Singleton {
} }
} }
root.workspaces = newWorkspaces setWorkspaces(newWorkspaces)
allWorkspaces = Object.values(newWorkspaces).sort((a, b) => a.idx - b.idx)
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused) focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
if (focusedWorkspaceIndex >= 0) { if (focusedWorkspaceIndex >= 0) {
@@ -325,18 +329,15 @@ Singleton {
updatedWorkspaces[id] = updatedWs updatedWorkspaces[id] = updatedWs
} }
root.workspaces = updatedWorkspaces setWorkspaces(updatedWorkspaces)
focusedWorkspaceId = data.id focusedWorkspaceId = data.id
focusedWorkspaceIndex = Object.values(updatedWorkspaces).findIndex(w => w.id === data.id) focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id)
if (focusedWorkspaceIndex >= 0) { if (focusedWorkspaceIndex >= 0) {
const ws = Object.values(updatedWorkspaces)[focusedWorkspaceIndex] currentOutput = allWorkspaces[focusedWorkspaceIndex].output || ""
currentOutput = ws.output || ""
} }
allWorkspaces = Object.values(updatedWorkspaces).sort((a, b) => a.idx - b.idx)
updateCurrentOutputWorkspaces() updateCurrentOutputWorkspaces()
} }
@@ -377,7 +378,7 @@ Singleton {
for (const id in root.workspaces) { for (const id in root.workspaces) {
updatedWorkspaces[id] = id === focusedWindow.workspace_id ? updatedWs : root.workspaces[id] updatedWorkspaces[id] = id === focusedWindow.workspace_id ? updatedWs : root.workspaces[id]
} }
root.workspaces = updatedWorkspaces setWorkspaces(updatedWorkspaces)
} }
} }
} }
@@ -395,7 +396,7 @@ Singleton {
for (const id in root.workspaces) { for (const id in root.workspaces) {
updatedWorkspaces[id] = id === data.workspace_id ? updatedWs : root.workspaces[id] updatedWorkspaces[id] = id === data.workspace_id ? updatedWs : root.workspaces[id]
} }
root.workspaces = updatedWorkspaces setWorkspaces(updatedWorkspaces)
} }
const updatedWindows = [] const updatedWindows = []
@@ -533,9 +534,7 @@ Singleton {
for (const id in root.workspaces) { for (const id in root.workspaces) {
updatedWorkspaces[id] = id === data.id ? updatedWs : root.workspaces[id] updatedWorkspaces[id] = id === data.id ? updatedWs : root.workspaces[id]
} }
root.workspaces = updatedWorkspaces setWorkspaces(updatedWorkspaces)
allWorkspaces = Object.values(updatedWorkspaces).sort((a, b) => a.idx - b.idx)
windowUrgentChanged() windowUrgentChanged()
} }
@@ -892,4 +891,17 @@ window-rule {
writeBindsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cp --no-preserve=mode "${sourceBindsPath}" "${bindsPath}"`] writeBindsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cp --no-preserve=mode "${sourceBindsPath}" "${bindsPath}"`]
writeBindsProcess.running = true writeBindsProcess.running = true
} }
function generateNiriBlurrule() {
console.log("NiriService: Generating wpblur config...")
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
const niriDmsDir = configDir + "/niri/dms"
const blurrulePath = niriDmsDir + "/wpblur.kdl"
const sourceBlurrulePath = Paths.strip(Qt.resolvedUrl("niri-wpblur.kdl"))
writeBindsProcess.bindsPath = blurrulePath
writeBindsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cp --no-preserve=mode "${sourceBlurrulePath}" "${blurrulePath}"`]
writeBindsProcess.running = true
}
} }

View File

@@ -573,6 +573,12 @@ Singleton {
function getPluginTrigger(pluginId) { function getPluginTrigger(pluginId) {
const plugin = getLauncherPlugin(pluginId) const plugin = getLauncherPlugin(pluginId)
if (plugin) { if (plugin) {
// Check if noTrigger is set (always active mode)
const noTrigger = SettingsData.getPluginSetting(pluginId, "noTrigger", false)
if (noTrigger) {
return ""
}
// Otherwise load the custom trigger, defaulting to plugin manifest trigger
const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!") const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!")
return customTrigger return customTrigger
} }

View File

@@ -6,6 +6,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.I3
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
@@ -184,7 +185,16 @@ Singleton {
return return
} }
// Hyprland fallback if (CompositorService.isDwl) {
DwlService.quit()
return
}
if (CompositorService.isSway) {
try { I3.dispatch("exit") } catch(_){}
return
}
Hyprland.dispatch("exit") Hyprland.dispatch("exit")
} else { } else {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionLogout]) Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionLogout])

View File

@@ -16,9 +16,6 @@ Singleton {
property var monitorTimers: ({}) property var monitorTimers: ({})
property var monitorLastTimeChecks: ({}) property var monitorLastTimeChecks: ({})
property var monitorProcesses: ({}) property var monitorProcesses: ({})
Component.onCompleted: {
updateCyclingState()
}
Component { Component {
id: monitorTimerComponent id: monitorTimerComponent

View File

@@ -322,7 +322,7 @@ Singleton {
Process { Process {
id: ipLocationFetcher id: ipLocationFetcher
command: lowPriorityCmd.concat(curlBaseCmd).concat(["http://ipinfo.io/json"]) command: lowPriorityCmd.concat(curlBaseCmd).concat(["http://ip-api.com/json/"])
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
@@ -335,23 +335,17 @@ Singleton {
try { try {
const data = JSON.parse(raw) const data = JSON.parse(raw)
const coords = data.loc
if (data.status === "fail") {
throw new Error("IP location lookup failed")
}
const lat = parseFloat(data.lat)
const lon = parseFloat(data.lon)
const city = data.city const city = data.city
if (!coords || !city) { if (!city || isNaN(lat) || isNaN(lon)) {
throw new Error("Missing location data") throw new Error("Missing or invalid location data")
}
const coordsParts = coords.split(",")
if (coordsParts.length !== 2) {
throw new Error("Invalid coordinates format")
}
const lat = parseFloat(coordsParts[0])
const lon = parseFloat(coordsParts[1])
if (isNaN(lat) || isNaN(lon)) {
throw new Error("Invalid coordinate values")
} }
root.location = { root.location = {

4
Services/niri-wpblur.kdl Normal file
View File

@@ -0,0 +1,4 @@
layer-rule {
match namespace="dms:blurwallpaper"
place-within-backdrop true
}

View File

@@ -1 +1 @@
v0.2.3 v0.3.0

View File

@@ -1,8 +1,8 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
StyledText { Item {
id: icon id: root
property alias name: icon.text property alias name: icon.text
property alias size: icon.font.pixelSize property alias size: icon.font.pixelSize
@@ -12,31 +12,45 @@ StyledText {
property int grade: Theme.isLightMode ? 0 : -25 property int grade: Theme.isLightMode ? 0 : -25
property int weight: filled ? 500 : 400 property int weight: filled ? 500 : 400
implicitWidth: icon.implicitWidth
implicitHeight: icon.implicitHeight
signal rotationCompleted() signal rotationCompleted()
font.family: "Material Symbols Rounded" FontLoader {
id: materialSymbolsFont
source: Qt.resolvedUrl("../assets/fonts/material-design-icons/variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf")
}
StyledText {
id: icon
anchors.fill: parent
font.family: materialSymbolsFont.name
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: weight font.weight: root.weight
color: Theme.surfaceText color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering renderType: Text.NativeRendering
antialiasing: true antialiasing: true
font.variableAxes: { font.variableAxes: {
"FILL": fill.toFixed(1), "FILL": root.fill.toFixed(1),
"GRAD": grade, "GRAD": root.grade,
"opsz": 24, "opsz": 24,
"wght": weight "wght": root.weight
} }
Behavior on fill { Behavior on font.weight {
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
}
Behavior on weight { Behavior on fill {
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
@@ -47,7 +61,7 @@ StyledText {
id: rotationTimer id: rotationTimer
interval: 16 interval: 16
repeat: false repeat: false
onTriggered: icon.rotationCompleted() onTriggered: root.rotationCompleted()
} }
onRotationChanged: { onRotationChanged: {

View File

@@ -79,7 +79,8 @@ ListView {
const lines = Math.floor(Math.abs(deltaY) / 120) const lines = Math.floor(Math.abs(deltaY) / 120)
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed
let newY = listView.contentY + scrollAmount let newY = listView.contentY + scrollAmount
newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY)) const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY)
newY = Math.max(listView.originY, Math.min(maxY, newY))
if (listView.flicking) { if (listView.flicking) {
listView.cancelFlick() listView.cancelFlick()
@@ -120,7 +121,8 @@ ListView {
} }
let newY = listView.contentY - delta let newY = listView.contentY - delta
newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY)) const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY)
newY = Math.max(listView.originY, Math.min(maxY, newY))
if (listView.flicking) { if (listView.flicking) {
listView.cancelFlick() listView.cancelFlick()
@@ -153,10 +155,11 @@ ListView {
onTriggered: { onTriggered: {
const newY = contentY - momentumVelocity * 0.016 const newY = contentY - momentumVelocity * 0.016
const maxY = Math.max(0, contentHeight - height) const maxY = Math.max(0, contentHeight - height + originY)
const minY = originY
if (newY < 0 || newY > maxY) { if (newY < minY || newY > maxY) {
contentY = newY < 0 ? 0 : maxY contentY = newY < minY ? minY : maxY
savedY = contentY savedY = contentY
stop() stop()
isMomentumActive = false isMomentumActive = false

143
Widgets/DankNFIcon.qml Normal file
View File

@@ -0,0 +1,143 @@
import QtQuick
import qs.Common
Item {
id: root
property string name: ""
property alias size: icon.font.pixelSize
property alias color: icon.color
implicitWidth: icon.implicitWidth
implicitHeight: icon.implicitHeight
visible: text.length > 0
// This is for file browser, particularly - might want another map later for app IDs
readonly property var iconMap: ({
// --- special types ---
"folder": "\u{F024B}",
"file": "\u{F0214}",
// --- special filenames (no extension) ---
"docker": "\u{F0868}",
"makefile": "\u{F09EE}",
"license": "\u{F09EE}",
"readme": "\u{F0354}",
// --- programming languages ---
"rs": "\u{F1617}",
"dart": "\u{e798}",
"go": "\u{F07D3}",
"py": "\u{F0320}",
"js": "\u{F031E}",
"jsx": "\u{F031E}",
"ts": "\u{F06E6}",
"tsx": "\u{F06E6}",
"java": "\u{F0B37}",
"c": "\u{F0671}",
"cpp": "\u{F0672}",
"cxx": "\u{F0672}",
"h": "\u{F0672}",
"hpp": "\u{F0672}",
"cs": "\u{F031B}",
"html": "\u{e60e}",
"htm": "\u{e60e}",
"css": "\u{E6b8}",
"scss": "\u{F031C}",
"less": "\u{F031C}",
"md": "\u{F0354}",
"markdown": "\u{F0354}",
"json": "\u{eb0f}",
"jsonc": "\u{eb0f}",
"yaml": "\u{e8eb}",
"yml": "\u{e8eb}",
"xml": "\u{F09EE}",
"sql": "\u{f1c0}",
// --- scripts / shells ---
"sh": "\u{f0bc1}",
"bash": "\u{f0bc1}",
"zsh": "\u{f0bc1}",
"fish": "\u{f0bc1}",
"ps1": "\u{f0bc1}",
"bat": "\u{f0bc1}",
// --- data / config ---
"toml": "\u{e6b2}",
"ini": "\u{F09EE}",
"conf": "\u{F09EE}",
"cfg": "\u{F09EE}",
"csv": "\u{eefc}",
"tsv": "\u{F021C}",
// --- docs / office ---
"pdf": "\u{F0226}",
"doc": "\u{F09EE}",
"docx": "\u{F09EE}",
"rtf": "\u{F09EE}",
"ppt": "\u{F09EE}",
"pptx": "\u{F09EE}",
"log": "\u{F09EE}",
"xls": "\u{F021C}",
"xlsx": "\u{F021C}",
// --- images ---
"ico": "\u{F021F}",
// --- audio / video ---
"mp3": "\u{e638}",
"wav": "\u{e638}",
"flac": "\u{e638}",
"ogg": "\u{e638}",
"mp4": "\u{f0567}",
"mkv": "\u{f0567}",
"webm": "\u{f0567}",
"mov": "\u{f0567}",
// --- archives / packages ---
"zip": "\u{e6aa}",
"tar": "\u{f003c}",
"gz": "\u{f003c}",
"bz2": "\u{f003c}",
"7z": "\u{f003c}",
// --- containers / infra / cloud ---
"dockerfile": "\u{F0868}",
"yml.k8s": "\u{F09EE}",
"yaml.k8s": "\u{F09EE}",
"tf": "\u{F09EE}",
"tfvars": "\u{F09EE}"
})
readonly property string text: iconMap[name] || iconMap["file"] || ""
function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase()
if (lowerName.startsWith("dockerfile")) {
return "docker"
}
const ext = fileName.split('.').pop()
return ext || ""
}
FontLoader {
id: firaCodeFont
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
}
StyledText {
id: icon
anchors.fill: parent
font.family: firaCodeFont.name
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
text: root.text
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
antialiasing: true
}
}

View File

@@ -75,17 +75,7 @@ PanelWindow {
readonly property real screenWidth: root.screen.width readonly property real screenWidth: root.screen.width
readonly property real screenHeight: root.screen.height readonly property real screenHeight: root.screen.height
readonly property real dpr: { readonly property real dpr: CompositorService.getScreenScale(root.screen)
if (CompositorService.isNiri && root.screen) {
const niriScale = NiriService.displayScales[root.screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && root.screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === root.screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return root.screen?.devicePixelRatio || 1
}
readonly property real alignedWidth: Theme.px(popupWidth, dpr) readonly property real alignedWidth: Theme.px(popupWidth, dpr)
readonly property real alignedHeight: Theme.px(popupHeight, dpr) readonly property real alignedHeight: Theme.px(popupHeight, dpr)

View File

@@ -57,6 +57,15 @@ PanelWindow {
WlrLayershell.exclusiveZone: 0 WlrLayershell.exclusiveZone: 0
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
mask: Region {
item: Rectangle {
x: root.width - contentRect.width
y: 0
width: contentRect.width
height: root.height
}
}
StyledRect { StyledRect {
id: contentRect id: contentRect
layer.enabled: true layer.enabled: true

View File

@@ -5,15 +5,22 @@ import qs.Services
Text { Text {
property bool isMonospace: false property bool isMonospace: false
FontLoader {
id: interFont
source: Qt.resolvedUrl("../assets/fonts/inter/InterVariable.ttf")
}
FontLoader {
id: firaCodeFont
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
}
readonly property string resolvedFontFamily: { readonly property string resolvedFontFamily: {
const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily
const defaultFont = isMonospace ? SettingsData.defaultMonoFontFamily : SettingsData.defaultFontFamily const defaultFont = isMonospace ? SettingsData.defaultMonoFontFamily : SettingsData.defaultFontFamily
if (requestedFont === defaultFont) { if (requestedFont === defaultFont) {
const availableFonts = Qt.fontFamilies() return isMonospace ? firaCodeFont.name : interFont.name
if (!availableFonts.includes(requestedFont)) {
return isMonospace ? "Monospace" : "DejaVu Sans"
}
} }
return requestedFont return requestedFont
} }

Binary file not shown.

View File

@@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,164 @@
# Inter
Inter is a typeface carefully crafted & designed for computer screens.
Inter features a tall x-height to aid in readability of mixed-case and lower-case text.
Inter is a [variable font](https://rsms.me/inter/#variable) with
several [OpenType features](https://rsms.me/inter/#features), like contextual alternates that adjusts punctuation depending on the shape of surrounding glyphs, slashed zero for when you need to disambiguate "0" from "o", tabular numbers, etc.
[**Download Inter font files…**](https://github.com/rsms/inter/releases/latest)
<br>
[![Sample](misc/readme/intro.png)](https://rsms.me/inter/)
### Quick questions
- **Where can I get Inter?** [Here](https://github.com/rsms/inter/releases/latest)
- **I think I found a bug. How can I let you know?** [Open an issue here](https://github.com/rsms/inter/issues/new?template=bug_report.md)
- **I have a question. Where can I get help?** [Post in Discussions Q&A](https://github.com/rsms/inter/discussions/categories/q-a)
- **Should I use Inter from Google Fonts?** No, unless you have no other choice.
(outdated, no italics)
- **Can I legally use Inter for my purpose?** Most likely _yes!_ Inter is free and open source.
([Read the license](LICENSE.txt) for details.)
## Using & installing Inter
[**Download the latest font files…**](https://github.com/rsms/inter/releases/latest)
Using Inter on a web page:
```html
<link rel="preconnect" href="https://your-font-file-host/">
<link rel="stylesheet" href="https://your-font-file-host/inter.css">
```
```css
:root { font-family: 'Inter', sans-serif; }
@supports (font-variation-settings: normal) {
:root { font-family: 'Inter var', sans-serif; }
}
```
For web pages, there's an official [CDN distribution](https://rsms.me/inter/inter.css) that you can use directly without having to host the font files yourself:
```html
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
```
### Alternate distributions
- [NPM `inter-ui`](https://www.npmjs.com/package/inter-ui)
- [Homebrew `font-inter`](https://github.com/Homebrew/homebrew-cask-fonts)
- [Ubuntu `fonts-inter`](https://packages.ubuntu.com/search?keywords=fonts-inter)
- [List of Inter available on various Linux distributions…](https://repology.org/project/fonts:inter/versions)
- [Google Fonts](https://fonts.google.com/specimen/Inter)
**Disclaimer:** Alternate distributions may not always be up-to-date.
## Notable uses of Inter
- [ElementaryOS](https://elementary.io/)
- [Figma](https://figma.com/)
- [GitLab](https://gitlab.com/)
- [Guggenheim museums](https://www.pentagram.com/work/guggenheim-3) ([case study](https://www.pentagram.com/work/guggenheim-3/))
- [ISO (International Organization for Standardization)](https://www.iso.org/) ([case study](https://www.motherbird.com.au/projects/iso/))
- [Minimalissimo magazine](https://minimalissimo.com/)
- [Mozilla](https://mozilla.design/firefox/typography/)
- [NASA](https://www.nasa.gov/specials/artemis-ii/)
- [Pixar Presto](https://en.wikipedia.org/wiki/Presto_(animation_software))
- [Switzerland, Canton of Zurich](https://www.zh.ch/)
- [Unity](https://unity.com/)
- [Zurich Airport](https://flughafen-zuerich.ch/)
> **Have you made something nice with Inter?**<br>
> [Please share in Show & Tell! →](https://github.com/rsms/inter/discussions/categories/show-and-tell)
### Notable forks
- [Open Runde](https://github.com/lauridskern/open-runde) is a rounded variant of Inter
- [Interalia](https://github.com/Shavian-info/interalia) extends Inter with Shavian characters
- [Raveo](https://github.com/jakubfoglar/raveo) is a "warmer version" of Inter
## Supporters & contributors
A wholehearted **Thank You** to everyone who supports the Inter project!
Special thanks to
[@thundernixon](https://github.com/thundernixon) and
[@KatjaSchimmel](https://github.com/KatjaSchimmel)
who have put in significant effort into making Inter what it is through
their contributions ♡
See [graphs/contributors](https://github.com/rsms/inter/graphs/contributors)
for a complete list of all contributors.
## Contributing to this project
For instructions on how to work with the source files and how to
[compile & build font files](CONTRIBUTING.md#compiling-font-files),
refer to [**CONTRIBUTING.md**](CONTRIBUTING.md).
Inter is licensed under the [SIL Open Font License](LICENSE.txt)
## Creating derivative fonts
Inter is open source which means you can make your own versions with your own changes.
However when doing so, please [**read LICENSE.txt carefully.**](LICENSE.txt) It is a standard **SIL Open Font License 1.1**:
> The goals of the Open Font License (OFL) are to stimulate worldwide
> development of collaborative font projects, to support the font creation
> efforts of academic and linguistic communities, and to provide a free and
> open framework in which fonts may be shared and improved in partnership
> with others.
>
> The OFL allows the licensed fonts to be used, studied, modified and
> redistributed freely as long as they are not sold by themselves. The
> fonts, including any derivative works, can be bundled, embedded,
> redistributed and/or sold with any software provided that any reserved
> names are not used by derivative works. The fonts and derivatives,
> however, cannot be released under any other type of license. The
> requirement for fonts to remain under this license does not apply
> to any document created using the fonts or their derivatives.
While you are allowed to use Inter commercially, i.e. bundled with product or service which makes you money, you are NOT allowed to sell Inter itself or derivatives of Inter. If you would like to do so, please [reach out](https://github.com/rsms) and we can talk about it.
Inter a trademark of Rasmus Andersson (DBA: RSMS)
"Inter" is a Reserved Font Name by Rasmus Andersson
([font vendor code RSMS.](https://learn.microsoft.com/en-us/typography/vendors/#r))
## Design
_This section discusses some of the design choices made for Inter._
Inter can be classified as a geometric neo-grotesque, similar in style to Roboto, Apple San Francisco, Akkurat, Asap, Lucida Grande and more. Some trade-offs were made in order to make this typeface work really well at small sizes:
- Early versions of Inter was not suitable for very large sizes because of some small-scale glyph optimizations (like "pits" and "traps") that help rasterization at small sizes but stand out and interfere at large sizes. However today Inter works well at large sizes and a [Display subfamily](https://github.com/rsms/inter/releases/tag/display-beta-1) is in the works for really large "display" sizes.
- Rasterized at sizes below 12px, some stems—like the horizontal center of "E", "F", or vertical center of "m"—are drawn with two semi-opaque pixels instead of one solid. This is because we "prioritize" (optimize for) higher-density rasterizations. If we move these stems to an off-center position—so that they can be drawn sharply at e.g. 11px—text will be less legible at higher resolutions.
Inter is a [variable font](https://rsms.me/inter/#variable) and is in addition also distributed as a set of traditional distinct font files in the following styles:
| Roman (upright) name | Italic name | Weight
| -------------------- | -------------------- | ------------
| Thin | Thin Italic | 100
| Extra Light | Extra Light Italic | 200
| Light | Light Italic | 300
| Regular | Italic | 400
| Medium | Medium Italic | 500
| Semi Bold | Semi Bold Italic | 600
| Bold | Bold Italic | 700
| Extra Bold | Extra Bold Italic | 800
| Black | Black Italic | 900

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,141 @@
## Material Symbols / Material Icons
These are two different official icon sets from Google, using the same underlying designs. Material Symbols is the current set, introduced in April 2022, built on variable font technology. Material Icons is the classic set, but no longer updated. More details below.
The icons can be browsed in a more user-friendly way at https://fonts.google.com/icons. Use the popdown menu near top left to choose between the two sets; Material Symbols is the default.
The icons are designed under the [material design guidelines](https://material.io/guidelines/).
## Icon Requests
Wed love to support your icon needs! Please submit your request here on GitHub as an issue.
Please note that Google Fonts does not accept user submissions of finished icon designs! There are fairly strict guidelines for Material icons, plus Google has upstream source files from which this repo is generated. Therefore, Google does not accept pull requests for icon files (whether new icon suggestions, or fixes for existing icons). Concepts are appreciated—just dont design SVGs and submit them via pull request.
However, users are perfectly welcome to point at outside files or images as examples—for the kind of thing they want, but they wont just be taken “as is.” This works especially well if you have multiple examples for a single icon, to help us understand the “essence” of the idea.
> For example, there is a fairly universal conceptual logo/icon for “agender,” so if you were proposing Google add an agender icon in the Material style, either mentioning that, or pointing at https://www.google.com/search?q=agender+icon would be a helpful tip.
### Third-party logos
Currently, Google does not include 3rd-party logos among the Material Symbols or Material Icons due to legal reasons. Some 3rd-party logos that were included in the past have since been removed.
## npm Packages
Google does not currently maintain the npm package for this repo, past v3 (2016). However, user @marella is hosting the following. He tells us these are automatically updated and published using GitHub Actions. Note: Google does **not** monitor or vet these packages.
### [material-symbols](https://github.com/marella/material-symbols/tree/main/material-symbols#readme) [![npm](https://img.shields.io/npm/v/material-symbols)](https://www.npmjs.com/package/material-symbols) [![install size](https://packagephobia.com/badge?p=material-symbols)](https://packagephobia.com/result?p=material-symbols)
- Only WOFF2 variable fonts and CSS for Material Symbols
- Includes outlined, rounded, and sharp icons and all variations of fill, weight, grade, and optical size
- Supports Sass
### [material-icons](https://github.com/marella/material-icons#readme) [![npm](https://img.shields.io/npm/v/material-icons)](https://www.npmjs.com/package/material-icons) [![install size](https://packagephobia.com/badge?p=material-icons)](https://packagephobia.com/result?p=material-icons) [![Downloads](https://img.shields.io/npm/dm/material-icons)](https://www.npmjs.com/package/material-icons)
- Only WOFF2, WOFF fonts and CSS
- Includes outlined, round, sharp and two-tone icons
- Supports Sass
### [@material-design-icons/font](https://github.com/marella/material-design-icons/tree/main/font#readme) [![npm (scoped)](https://img.shields.io/npm/v/@material-design-icons/font)](https://www.npmjs.com/package/@material-design-icons/font) [![install size](https://packagephobia.com/badge?p=@material-design-icons/font)](https://packagephobia.com/result?p=@material-design-icons/font)
- Only WOFF2 fonts and CSS
- Lighter version of `material-icons` package
- Doesn't support [older browsers](https://caniuse.com/woff2) such as Internet Explorer because of dropping WOFF (v1)
### [@material-design-icons/svg](https://github.com/marella/material-design-icons/tree/main/svg#readme) [![npm (scoped)](https://img.shields.io/npm/v/@material-design-icons/svg)](https://www.npmjs.com/package/@material-design-icons/svg) [![install size](https://packagephobia.com/badge?p=@material-design-icons/svg)](https://packagephobia.com/result?p=@material-design-icons/svg)
- Only SVGs
- Optimizes SVGs using SVGO
## Material Symbols
These newer icons can be browsed in a more user-friendly way at https://fonts.google.com/icons. Use the popdown menu near top left to choose between the two sets; Material Symbols is the default.
These icons were built/designed as variable fonts first (based on the 24 px designs from Material Icons). There are three separate Material Symbols variable fonts, which also have static icons available (but those do not have all the variations available, as that would be hundreds of styles):
- Outlined
- Rounded
- Sharp
- Note that although there is no separate Filled font, the Fill axis allows access to filled styles, in all three fonts. It can also be manipulated for an animated fill effect, to indicate user selection.
Each of the fonts has these design axes, which can be varied in CSS, or in many more modern design apps:
- Optical Size (opsz) from 20 to 48 px. The default is 24.
- Weight from 100 (Thin) to 700 (Bold). Regular is 400.
- Grade from -50 to 200. The default is 0 (zero). -50 is suggested for reversed contrast (e.g. white icons on black background)
- Fill from 0 to 100. The default is 0 (zero).
The following directories in this repo contain specifically Material Symbols (not Material Icons) content:
- symbols
- variablefont
What is currently _not_ available in Material Symbols?
- only the 20 and 24 px versions are designed with perfect pixel-grid alignment
- the only pre-made fonts are the variable fonts
- there are no two-tone icons
## Material Icons
The icons can be browsed in a more user-friendly way at https://fonts.google.com/icons?icon.set=Material+Icons
These classic icons are available in five distinct styles:
- Outlined
- Filled (the font version is just called Material Icons, as this is the oldest style)
- Rounded
- Sharp
- Two tone
The following directories in this repo contain specifically Material Icons (not Material Symbols) content:
- android
- font
- ios
- png
- src
What is currently _not_ available in Material Icons?
- variable fonts
- weights other than Regular
- grades other than Regular
- a means to animate Fill transitions
- new icons (since updates were halted in 2022)
## Material Icons update history
### 4.0.0 Update
* 2020 Aug 31
* Restructured repository, updated assets.
### 3.0.1 Update
* 2016 Sep 01
* Changed license in package.json.
* Added missing device symbol sprites.
### 3.0.0 Update
* 2016 Aug 25
* License change to Apache 2.0!
### 2.0
* 2016 May 28
## Getting Started
Read the [developer guide](https://developers.google.com/fonts/docs/material_icons) on how to use the material design icons in your project.
### Using a font
The `font` and `variablefont` folders contain pre-generated font files that can be included in a project. This is especially convenient for the web; however, it is generally better to link to the web font hosted on Google Fonts:
```html
<link href="https://fonts.googleapis.com/css2?family=Material+Icons"
rel="stylesheet">
```
```html
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined"
rel="stylesheet">
```
Read more on [Material Symbols](https://developers.google.com/fonts/docs/material_symbols/) or [Material Icons](https://developers.google.com/fonts/docs/material_icons/) in the Google Fonts developer guide.
## License
We have made these icons available for you to incorporate into your products under the [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt). Feel free to remix and re-share these icons and documentation in your products.
We'd love attribution in your app's *about* screen, but it's not required.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More