1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-25 05:52:50 -05:00

Compare commits

...

40 Commits

Author SHA1 Message Date
purian23
ec8ab47462 distros: Deprecate Cliphist dependencies 2026-01-17 01:06:28 -05:00
purian23
35cbfeb008 feat: Save Pinned Clipboard entries 2026-01-17 00:52:47 -05:00
bbedward
7036362b9b dgop: fix default sort direction 2026-01-16 21:04:44 -05:00
bbedward
2bcb33e85c system monitor: update gauge sizes 2026-01-16 20:28:57 -05:00
bbedward
76ac036f85 system monitor: overhaul popout and app with new design 2026-01-16 20:20:03 -05:00
bbedward
581073394a dank16: update algorithm overall
- More similarities to primary, smoother gradient of
  cyan->purple->magenta->white, keep gray
- Make purple slot near-match for primaryContainer
2026-01-16 18:10:15 -05:00
Flux
d7b7086b21 labwc patch (#1391) 2026-01-16 09:50:01 -05:00
bbedward
59be179821 i18n: more RTL fixes across settings 2026-01-16 09:45:15 -05:00
bbedward
1cf2f6b946 popout: fix cross-monitor handling of widgets
fixes #1364
2026-01-16 09:45:15 -05:00
bbedward
a57a9c2121 doctor: add mango and labwc to compositors
fixes #1394
2026-01-16 09:45:15 -05:00
bbedward
67568c3746 greeter: remove WLR_DRM_DEVICES setting
fixes #1393
2026-01-16 09:45:15 -05:00
bbedward
afce792b80 dankbar: fix property preservation in widgets
fixes #1392
2026-01-16 09:45:15 -05:00
bbedward
f5c7493dbb weather: fix precipitationw weekly propability
fixes #1395
2026-01-16 09:45:15 -05:00
bbedward
f9b9d98638 plugins: ensure daemon plugins not instantiated twice 2026-01-16 09:45:15 -05:00
bbedward
2a97e03fa6 cc: fixed width column, remove anchoring from individual icons on vbar
maybe #1376
2026-01-16 09:45:15 -05:00
Lucas
d6dacc2975 nix: fix home module (#1387) 2026-01-16 08:46:26 +01:00
Bailey
aab4b6765d nix: Support specifying systemd target (#1385) 2026-01-16 02:01:51 -03:00
bbedward
3539aca1f7 cc: wrap icons in fixed size containers
maybe #1376
2026-01-15 23:05:21 -05:00
bbedward
81fbe9eaba controlcenter: fix visibility condition of no icons
fixes #1377
2026-01-15 23:00:43 -05:00
purian23
f9dc6de485 Fix fedora version format 2026-01-15 23:00:08 -05:00
bbedward
012022d370 plugins: fix plugin confirm third part repo window 2026-01-15 22:55:11 -05:00
purian23
993216e157 distro: Update Fedora dynamic versioning 2026-01-15 22:30:20 -05:00
purian23
c992f2b582 feat: Allow more pinned services in Control Center/Settings 2026-01-15 21:51:17 -05:00
purian23
3243adebca core: Update ghostty on dankinstall 2026-01-15 21:26:31 -05:00
Abhinav Chalise
baccef57d4 fix volume osd sliding ui update for vertical layout (#1382) 2026-01-15 21:10:43 -05:00
bbedward
a823095372 widgets: add fallback for steam apps 2026-01-15 21:07:57 -05:00
Lucas
172a743de4 doctor: use dbus for checking on services (#1384)
* doctor: use dbus for checking on services

* doctor: show docs URL for failed checks

* core: remove unused function
2026-01-15 20:59:47 -05:00
Ivan Molodetskikh
623eec3689 Add screencast indicator for niri (#1361)
* niri: Handle new Cast events

* bar: Add screen sharing indicator

Configurable like other icons; on by default.

* lockscreen: Add screen sharing indicator
2026-01-15 00:55:21 -05:00
bbedward
53a033fe35 dankdash: fix weather open IPC
fixes #1367
2026-01-14 22:29:29 -05:00
bbedward
c490ee24f4 matugen: fix nvim ID in skipTemplates 2026-01-14 22:27:07 -05:00
bbedward
cc1e49294e i18n: update terms 2026-01-14 22:22:27 -05:00
purian23
e6fa46ae26 dankdash: Center Media Art & Controls 2026-01-14 18:03:16 -05:00
purian23
35fe774a1b Update OBS Choice selection 2026-01-13 17:51:55 -05:00
purian23
1e6a0f9423 Update OBS DMS Stable workflow 2026-01-13 17:31:01 -05:00
bbedward
cc1877aadb modals: fix wifi passowrd, polkit, and VPN import 2026-01-13 17:21:16 -05:00
bbedward
f1eb1fa9ba settings: fix child windows on newer quickshell-git 2026-01-13 16:57:23 -05:00
Lucas
bdd01e335d settings: fix modal not opening on latest quickshell (#1357) 2026-01-13 16:41:54 -05:00
Lucas
4b7baf82cd nix: escape version string (#1353) 2026-01-13 11:24:51 -05:00
purian23
15c88ce1d2 quickshell: Despace Versioning 2026-01-13 11:07:12 -05:00
bbedward
8891c388d0 bump to v1.4-unstable 2026-01-13 08:40:56 -05:00
92 changed files with 5513 additions and 4467 deletions

View File

@@ -4,13 +4,14 @@ on:
workflow_dispatch:
inputs:
package:
description: "Package to update (dms, dms-git, or all)"
required: false
default: "all"
tag_version:
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
required: false
default: ""
description: "Package to update"
required: true
type: choice
options:
- dms
- dms-git
- all
default: "dms"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
required: false
@@ -56,8 +57,9 @@ jobs:
}
# Helper function to check dms stable tag
# Sets LATEST_TAG variable in parent scope if update needed
check_dms_stable() {
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
@@ -73,8 +75,8 @@ jobs:
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag push - always update stable package
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag selected or pushed - always update stable package
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
@@ -104,7 +106,12 @@ jobs:
# Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -129,6 +136,9 @@ jobs:
if check_dms_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
@@ -161,12 +171,19 @@ jobs:
- name: Determine packages to update
id: packages
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag push event - use the pushed tag
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag selected or pushed - use the tag from GITHUB_REF
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION"
echo "Using tag from GITHUB_REF: $VERSION"
# Check if check-updates already determined a version (from auto-detection)
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
# Use version from check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - dms-git only
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
@@ -176,22 +193,28 @@ jobs:
# Determine version for dms stable
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
# For explicit dms selection, require tag_version
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
VERSION="${{ github.event.inputs.tag_version }}"
# Use github.ref if tag selected, otherwise auto-detect latest
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using specified tag: $VERSION"
echo "Using tag from GITHUB_REF: $VERSION"
else
echo "ERROR: tag_version is required when package=dms"
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
exit 1
# Auto-detect latest release for dms
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Auto-detected latest release: $LATEST_TAG"
else
echo "ERROR: Could not auto-detect latest release"
exit 1
fi
fi
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
# For "all", auto-detect if tag_version not specified
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
VERSION="${{ github.event.inputs.tag_version }}"
# Use github.ref if tag selected, otherwise auto-detect latest
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using specified tag: $VERSION"
echo "Using tag from GITHUB_REF: $VERSION"
else
# Auto-detect latest release for "all"
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
@@ -206,7 +229,7 @@ jobs:
fi
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
else
@@ -215,6 +238,9 @@ jobs:
fi
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
fi
fi
- name: Update dms-git spec version

View File

@@ -87,6 +87,8 @@ var (
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
)
var doctorCmd = &cobra.Command{
@@ -448,11 +450,13 @@ func checkWindowManagers() []checkResult {
versionRegex *regexp.Regexp
commands []string
}{
{"Hyprland", "hyprctl", "version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
{"Hyprland", "Hyprland", "--version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
}
var results []checkResult
@@ -477,7 +481,7 @@ func checkWindowManagers() []checkResult {
results = append(results, checkResult{
catCompositor, c.name, statusOK,
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
doctorDocsURL + "#compositor",
doctorDocsURL + "#compositor-checks",
})
}
@@ -486,7 +490,7 @@ func checkWindowManagers() []checkResult {
catCompositor, "Compositor", statusError,
"No supported Wayland compositor found",
"Install Hyprland, niri, Sway, River, or Wayfire",
doctorDocsURL + "#compositor",
doctorDocsURL + "#compositor-checks",
})
}
@@ -498,8 +502,8 @@ func checkWindowManagers() []checkResult {
}
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
output, err := exec.Command(cmd, arg).Output()
if err != nil {
output, err := exec.Command(cmd, arg).CombinedOutput()
if err != nil && len(output) == 0 {
return "installed"
}
@@ -634,19 +638,14 @@ func checkI2CAvailability() checkResult {
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
}
func detectNetworkBackend() string {
result, err := network.DetectNetworkStack()
if err != nil {
return ""
}
switch result.Backend {
func detectNetworkBackend(stackResult *network.DetectResult) string {
switch stackResult.Backend {
case network.BackendNetworkManager:
return "NetworkManager"
case network.BackendIwd:
return "iwd"
case network.BackendNetworkd:
if result.HasIwd {
if stackResult.HasIwd {
return "iwd + systemd-networkd"
}
return "systemd-networkd"
@@ -657,75 +656,73 @@ func detectNetworkBackend() string {
}
}
func getOptionalDBusStatus(busName string) (status, string) {
if utils.IsDBusServiceAvailable(busName) {
return statusOK, "Available"
} else {
return statusWarn, "Not available"
}
}
func checkOptionalDependencies() []checkResult {
var results []checkResult
if utils.IsServiceActive("accounts-daemon", false) {
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusOK, "Running", "User accounts", doctorDocsURL + "#optional-features"})
} else {
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusWarn, "Not running", "User accounts", doctorDocsURL + "#optional-features"})
}
optionalFeaturesURL := doctorDocsURL + "#optional-features"
if utils.IsServiceActive("power-profiles-daemon", false) {
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusOK, "Running", "Power profile management", doctorDocsURL + "#optional-features"})
} else {
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusInfo, "Not running", "Power profile management", doctorDocsURL + "#optional-features"})
}
accountsStatus, accountsMsg := getOptionalDBusStatus("org.freedesktop.Accounts")
results = append(results, checkResult{catOptionalFeatures, "accountsservice", accountsStatus, accountsMsg, "User accounts", optionalFeaturesURL})
ppdStatus, ppdMsg := getOptionalDBusStatus("org.freedesktop.UPower.PowerProfiles")
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", ppdStatus, ppdMsg, "Power profile management", optionalFeaturesURL})
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
results = append(results, checkI2CAvailability())
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", doctorDocsURL + "#optional-features"})
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
} else {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", doctorDocsURL + "#optional-features"})
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
}
networkResult, err := network.DetectNetworkStack()
networkStatus, networkMessage, networkDetails := statusOK, "Not available", "Network management"
if err == nil && networkResult.Backend != network.BackendNone {
networkMessage = detectNetworkBackend(networkResult)
if doctorVerbose {
networkDetails = networkResult.ChosenReason
}
} else {
networkStatus = statusInfo
}
results = append(results, checkResult{catOptionalFeatures, "Network", networkStatus, networkMessage, networkDetails, optionalFeaturesURL})
deps := []struct {
name, cmd, altCmd, desc string
important bool
name, cmd, desc string
important bool
}{
{"matugen", "matugen", "", "Dynamic theming", true},
{"dgop", "dgop", "", "System monitoring", true},
{"cava", "cava", "", "Audio visualizer", true},
{"khal", "khal", "", "Calendar events", false},
{"Network", "nmcli", "iwctl", "Network management", false},
{"danksearch", "dsearch", "", "File search", false},
{"loginctl", "loginctl", "", "Session management", false},
{"fprintd", "fprintd-list", "", "Fingerprint auth", false},
{"matugen", "matugen", "Dynamic theming", true},
{"dgop", "dgop", "System monitoring", true},
{"cava", "cava", "Audio visualizer", true},
{"khal", "khal", "Calendar events", false},
{"danksearch", "dsearch", "File search", false},
{"fprintd", "fprintd-list", "Fingerprint auth", false},
}
for _, d := range deps {
found, foundCmd := utils.CommandExists(d.cmd), d.cmd
if !found && d.altCmd != "" && utils.CommandExists(d.altCmd) {
found, foundCmd = true, d.altCmd
}
found := utils.CommandExists(d.cmd)
switch {
case found:
message := "Installed"
details := d.desc
if d.name == "Network" {
result, err := network.DetectNetworkStack()
if err == nil && result.Backend != network.BackendNone {
message = detectNetworkBackend() + " (active)"
if doctorVerbose {
details = result.ChosenReason
}
} else {
switch foundCmd {
case "nmcli":
message = "NetworkManager (installed)"
case "iwctl":
message = "iwd (installed)"
}
}
}
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, message, details, doctorDocsURL + "#optional-features"})
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, "Installed", d.desc, optionalFeaturesURL})
case d.important:
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, doctorDocsURL + "#optional-features"})
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, optionalFeaturesURL})
default:
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, doctorDocsURL + "#optional-features"})
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, optionalFeaturesURL})
}
}
@@ -893,6 +890,10 @@ func printResultLine(r checkResult, styles tui.Styles) {
if doctorVerbose && r.details != "" {
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
}
if (r.status == statusError || r.status == statusWarn) && r.url != "" {
fmt.Printf(" %s\n", styles.Subtle.Render("→ "+r.url))
}
}
func printSummary(results []checkResult, qsMissingFeatures bool) {

View File

@@ -199,31 +199,6 @@ func labToHex(L, a, b float64) string {
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
}
// Adjust brightness while keeping the same hue
func retoneToL(hex string, Ltarget float64) string {
rgb := HexToRGB(hex)
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
L, a, b := col.Lab()
L100 := L * 100.0
scale := 1.0
if L100 != 0 {
scale = Ltarget / L100
}
a2, b2 := a*scale, b*scale
// Don't let it get too saturated
maxChroma := 0.4
if math.Hypot(a2, b2) > maxChroma {
k := maxChroma / math.Hypot(a2, b2)
a2 *= k
b2 *= k
}
return labToHex(Ltarget, a2, b2)
}
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
Lf := getLstar(hexFg)
Lb := getLstar(hexBg)
@@ -356,6 +331,59 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
return hexColor
}
// Bidirectional contrast - tries both lighter and darker, picks closest to original
func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLightMode bool) string {
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
if current >= minLc {
return hexColor
}
fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
origL, af, bf := cf.Lab()
var darkerResult, lighterResult string
darkerL, lighterL := origL, origL
darkerFound, lighterFound := false, false
step := 0.5
for i := range 120 {
if !darkerFound {
darkerL = math.Max(0, origL-float64(i)*step)
cand := labToHex(darkerL, af, bf)
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
darkerResult = cand
darkerFound = true
}
}
if !lighterFound {
lighterL = math.Min(100, origL+float64(i)*step)
cand := labToHex(lighterL, af, bf)
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
lighterResult = cand
lighterFound = true
}
}
if darkerFound && lighterFound {
break
}
}
if darkerFound && lighterFound {
if math.Abs(darkerL-origL) <= math.Abs(lighterL-origL) {
return darkerResult
}
return lighterResult
}
if darkerFound {
return darkerResult
}
if lighterFound {
return lighterResult
}
return hexColor
}
type PaletteOptions struct {
IsLight bool
Background string
@@ -369,6 +397,29 @@ func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOpti
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
}
func ensureContrastBidirectional(hexColor, hexBg string, target float64, opts PaletteOptions) string {
if opts.UseDPS {
return EnsureContrastDPSBidirectional(hexColor, hexBg, target, opts.IsLight)
}
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
}
func blendHue(base, target, factor float64) float64 {
diff := target - base
if diff > 0.5 {
diff -= 1.0
} else if diff < -0.5 {
diff += 1.0
}
result := base + diff*factor
if result < 0 {
result += 1.0
} else if result >= 1.0 {
result -= 1.0
}
return result
}
func DeriveContainer(primary string, isLight bool) string {
rgb := HexToRGB(primary)
hsv := RGBToHSV(rgb)
@@ -389,6 +440,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb)
pr := HexToRGB(primaryColor)
ph := RGBToHSV(pr)
var palette Palette
var normalTextTarget, secondaryTarget float64
@@ -410,115 +464,136 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
}
palette.Color0 = NewColorInfo(bgColor)
hueShift := (hsv.H - 0.6) * 0.12
satBoost := 1.15
baseSat := math.Max(ph.S, 0.5)
baseVal := math.Max(ph.V, 0.5)
redH := math.Mod(0.0+hueShift+1.0, 1.0)
var redColor string
if opts.IsLight {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
} else {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
}
redH := blendHue(0.0, ph.H, 0.12)
greenH := blendHue(0.33, ph.H, 0.10)
yellowH := blendHue(0.14, ph.H, 0.04)
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
var greenColor string
if opts.IsLight {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} else {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
}
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
var yellowColor string
if opts.IsLight {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
} else {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
}
var blueColor string
if opts.IsLight {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} else {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
}
magH := hsv.H - 0.03
if magH < 0 {
magH += 1.0
}
var magColor string
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
if opts.IsLight {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} else {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
}
cyanH := hsv.H + 0.08
if cyanH > 1.0 {
cyanH -= 1.0
}
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
accentTarget := secondaryTarget * 0.7
if opts.IsLight {
palette.Color7 = NewColorInfo("#1a1a1a")
palette.Color8 = NewColorInfo("#2e2e2e")
} else {
palette.Color7 = NewColorInfo("#abb2bf")
palette.Color8 = NewColorInfo("#5c6370")
}
redS := math.Min(baseSat*1.2, 1.0)
redV := baseVal * 0.95
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
if opts.IsLight {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} else {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
brightBlue := retoneToL(primaryColor, 85.0)
palette.Color12 = NewColorInfo(brightBlue)
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyanH := hsv.H + 0.02
if brightCyanH > 1.0 {
brightCyanH -= 1.0
}
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
}
greenS := math.Min(baseSat*1.3, 1.0)
greenV := baseVal * 0.75
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
if opts.IsLight {
palette.Color15 = NewColorInfo("#1a1a1a")
yellowS := math.Min(baseSat*1.5, 1.0)
yellowV := math.Min(baseVal*1.2, 1.0)
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
blueS := math.Min(ph.S*1.05, 1.0)
blueV := math.Min(ph.V*1.05, 1.0)
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
// Color5 matches primary_container exactly (light container in light mode)
container5 := DeriveContainer(primaryColor, true)
palette.Color5 = NewColorInfo(container5)
palette.Color6 = NewColorInfo(primaryColor)
gray7S := baseSat * 0.08
gray7V := baseVal * 0.28
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.05
gray8V := baseVal * 0.85
dimTarget := secondaryTarget * 0.5
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
brightRedS := math.Min(baseSat*1.0, 1.0)
brightRedV := math.Min(baseVal*1.2, 1.0)
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
brightGreenS := math.Min(baseSat*1.1, 1.0)
brightGreenV := math.Min(baseVal*1.1, 1.0)
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
brightYellowS := math.Min(baseSat*1.4, 1.0)
brightYellowV := math.Min(baseVal*1.3, 1.0)
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
brightBlueS := math.Min(ph.S*1.1, 1.0)
brightBlueV := math.Min(ph.V*1.15, 1.0)
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
lightContainer := DeriveContainer(primaryColor, true)
palette.Color13 = NewColorInfo(lightContainer)
brightCyanS := ph.S * 0.5
brightCyanV := math.Min(ph.V*1.3, 1.0)
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightCyanS, V: brightCyanV})))
white15S := baseSat * 0.04
white15V := math.Min(baseVal*1.5, 1.0)
palette.Color15 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})))
} else {
palette.Color15 = NewColorInfo("#ffffff")
redS := math.Min(baseSat*1.1, 1.0)
redV := math.Min(baseVal*1.15, 1.0)
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
greenS := math.Min(baseSat*1.0, 1.0)
greenV := math.Min(baseVal*1.0, 1.0)
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
yellowS := math.Min(baseSat*1.1, 1.0)
yellowV := math.Min(baseVal*1.25, 1.0)
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
// Slightly more saturated variant of primary
blueS := math.Min(ph.S*1.2, 1.0)
blueV := ph.V * 0.95
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
// Color5 matches primary_container exactly (dark container in dark mode)
darkContainer := DeriveContainer(primaryColor, false)
palette.Color5 = NewColorInfo(darkContainer)
palette.Color6 = NewColorInfo(primaryColor)
gray7S := baseSat * 0.12
gray7V := math.Min(baseVal*1.05, 1.0)
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.15
gray8V := baseVal * 0.65
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
brightRedS := math.Min(baseSat*0.75, 1.0)
brightRedV := math.Min(baseVal*1.35, 1.0)
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
brightGreenS := math.Min(baseSat*0.7, 1.0)
brightGreenV := math.Min(baseVal*1.2, 1.0)
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
brightYellowS := math.Min(baseSat*0.7, 1.0)
brightYellowV := math.Min(baseVal*1.5, 1.0)
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
// Create a gradient of primary variants: Color12 -> Color13 -> Color14 -> Color15 (near white)
// Color12: Start of the lighter gradient - slightly desaturated
brightBlueS := ph.S * 0.85
brightBlueV := math.Min(ph.V*1.1, 1.0)
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
// Medium-high saturation pastel primary
color13S := ph.S * 0.7
color13V := math.Min(ph.V*1.3, 1.0)
palette.Color13 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})))
// Lower saturation, lighter variant
color14S := ph.S * 0.45
color14V := math.Min(ph.V*1.4, 1.0)
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color14S, V: color14V})))
white15S := baseSat * 0.05
white15V := math.Min(baseVal*1.45, 1.0)
palette.Color15 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})), bgColor, normalTextTarget, opts))
}
return palette

View File

@@ -366,10 +366,19 @@ func TestGeneratePalette(t *testing.T) {
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
}
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
// Color15 is now derived from primary, so just verify it's a valid color
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
color15Lum := Luminance(result.Color15.Hex)
if tt.opts.IsLight {
// Light mode: Color15 should still be relatively light
if color15Lum < 0.5 {
t.Errorf("Light mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
}
} else {
// Dark mode: Color15 should be light (but may have theme tint, so lower threshold)
if color15Lum < 0.5 {
t.Errorf("Dark mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
}
}
})
}
@@ -579,6 +588,10 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
bgColor := result.Color0.Hex
for i := 1; i < 8; i++ {
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
if i == 5 || i == 6 {
continue
}
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
minLc := 30.0
if lc < minLc && lc > 0 {

View File

@@ -108,7 +108,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
packages := map[string]PackageMapping{
// Standard zypper packages
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
@@ -117,6 +116,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
// DMS packages from OBS
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}

View File

@@ -37,6 +37,14 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
handleSetConfig(conn, req, m)
case "clipboard.store":
handleStore(conn, req, m)
case "clipboard.pinEntry":
handlePinEntry(conn, req, m)
case "clipboard.unpinEntry":
handleUnpinEntry(conn, req, m)
case "clipboard.getPinnedEntries":
handleGetPinnedEntries(conn, req, m)
case "clipboard.getPinnedCount":
handleGetPinnedCount(conn, req, m)
default:
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
}
@@ -205,6 +213,9 @@ func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
if v, ok := models.Get[bool](req, "disabled"); ok {
cfg.Disabled = v
}
if v, ok := models.Get[float64](req, "maxPinned"); ok {
cfg.MaxPinned = int(v)
}
if err := m.SetConfig(cfg); err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -230,3 +241,43 @@ func handleStore(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
}
func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := m.PinEntry(uint64(id)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
}
func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := m.UnpinEntry(uint64(id)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
}
func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
pinned := m.GetPinnedEntries()
models.Respond(conn, req.ID, pinned)
}
func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
count := m.GetPinnedCount()
models.Respond(conn, req.ID, map[string]int{"count": count})
}

View File

@@ -389,7 +389,11 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
}
c := b.Cursor()
var count int
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v)
if err == nil && entry.Pinned {
continue
}
if count < m.config.MaxHistory {
count++
continue
@@ -419,6 +423,11 @@ func encodeEntry(e Entry) ([]byte, error) {
buf.WriteByte(0)
}
binary.Write(buf, binary.BigEndian, e.Hash)
if e.Pinned {
buf.WriteByte(1)
} else {
buf.WriteByte(0)
}
return buf.Bytes(), nil
}
@@ -462,6 +471,12 @@ func decodeEntry(data []byte) (Entry, error) {
binary.Read(buf, binary.BigEndian, &e.Hash)
}
if buf.Len() >= 1 {
var pinnedByte byte
binary.Read(buf, binary.BigEndian, &pinnedByte)
e.Pinned = pinnedByte == 1
}
return e, nil
}
@@ -735,19 +750,54 @@ func (m *Manager) ClearHistory() {
return
}
// Delete only non-pinned entries
if err := m.db.Update(func(tx *bolt.Tx) error {
if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
return err
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return nil
}
_, err := tx.CreateBucket([]byte("clipboard"))
return err
var toDelete [][]byte
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
if err != nil || !entry.Pinned {
toDelete = append(toDelete, k)
}
}
for _, k := range toDelete {
if err := b.Delete(k); err != nil {
return err
}
}
return nil
}); err != nil {
log.Errorf("Failed to clear clipboard history: %v", err)
return
}
if err := m.compactDB(); err != nil {
log.Errorf("Failed to compact database: %v", err)
pinnedCount := 0
if err := m.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b != nil {
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, _ := decodeEntry(v)
if entry.Pinned {
pinnedCount++
}
}
}
return nil
}); err != nil {
log.Errorf("Failed to count pinned entries: %v", err)
}
if pinnedCount == 0 {
if err := m.compactDB(); err != nil {
log.Errorf("Failed to compact database: %v", err)
}
}
m.updateState()
@@ -960,6 +1010,10 @@ func (m *Manager) clearOldEntries(days int) error {
if err != nil {
continue
}
// Skip pinned entries
if entry.Pinned {
continue
}
if entry.Timestamp.Before(cutoff) {
toDelete = append(toDelete, k)
}
@@ -1250,3 +1304,153 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
return nil
}
func (m *Manager) PinEntry(id uint64) error {
if m.db == nil {
return fmt.Errorf("database not available")
}
// Check pinned count
cfg := m.getConfig()
pinnedCount := 0
if err := m.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return nil
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
if err == nil && entry.Pinned {
pinnedCount++
}
}
return nil
}); err != nil {
log.Errorf("Failed to count pinned entries: %v", err)
}
if pinnedCount >= cfg.MaxPinned {
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
}
err := m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
v := b.Get(itob(id))
if v == nil {
return fmt.Errorf("entry not found")
}
entry, err := decodeEntry(v)
if err != nil {
return err
}
entry.Pinned = true
encoded, err := encodeEntry(entry)
if err != nil {
return err
}
return b.Put(itob(id), encoded)
})
if err == nil {
m.updateState()
m.notifySubscribers()
}
return err
}
func (m *Manager) UnpinEntry(id uint64) error {
if m.db == nil {
return fmt.Errorf("database not available")
}
err := m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
v := b.Get(itob(id))
if v == nil {
return fmt.Errorf("entry not found")
}
entry, err := decodeEntry(v)
if err != nil {
return err
}
entry.Pinned = false
encoded, err := encodeEntry(entry)
if err != nil {
return err
}
return b.Put(itob(id), encoded)
})
if err == nil {
m.updateState()
m.notifySubscribers()
}
return err
}
func (m *Manager) GetPinnedEntries() []Entry {
if m.db == nil {
return nil
}
var pinned []Entry
if err := m.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return nil
}
c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v)
if err != nil {
continue
}
if entry.Pinned {
entry.Data = nil
pinned = append(pinned, entry)
}
}
return nil
}); err != nil {
log.Errorf("Failed to get pinned entries: %v", err)
}
return pinned
}
func (m *Manager) GetPinnedCount() int {
if m.db == nil {
return 0
}
count := 0
if err := m.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return nil
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
if err == nil && entry.Pinned {
count++
}
}
return nil
}); err != nil {
log.Errorf("Failed to count pinned entries: %v", err)
}
return count
}

View File

@@ -19,6 +19,7 @@ type Config struct {
AutoClearDays int `json:"autoClearDays"`
ClearAtStartup bool `json:"clearAtStartup"`
Disabled bool `json:"disabled"`
MaxPinned int `json:"maxPinned"`
}
func DefaultConfig() Config {
@@ -27,6 +28,7 @@ func DefaultConfig() Config {
MaxEntrySize: 5 * 1024 * 1024,
AutoClearDays: 0,
ClearAtStartup: false,
MaxPinned: 25,
}
}
@@ -100,6 +102,7 @@ type Entry struct {
Timestamp time.Time `json:"timestamp"`
IsImage bool `json:"isImage"`
Hash uint64 `json:"hash,omitempty"`
Pinned bool `json:"pinned"`
}
type State struct {

View File

@@ -0,0 +1,20 @@
package utils
import (
"github.com/godbus/dbus/v5"
)
func IsDBusServiceAvailable(busName string) bool {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return false
}
defer conn.Close()
obj := conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
var owned bool
if err := obj.Call("org.freedesktop.DBus.NameHasOwner", 0, busName).Store(&owned); err != nil {
return false
}
return owned
}

View File

@@ -2,7 +2,6 @@ package utils
import (
"os/exec"
"strings"
)
type AppChecker interface {
@@ -43,16 +42,3 @@ func AnyCommandExists(cmds ...string) bool {
}
return false
}
func IsServiceActive(name string, userService bool) bool {
if !CommandExists("systemctl") {
return false
}
args := []string{"is-active", name}
if userService {
args = []string{"--user", "is-active", name}
}
output, _ := exec.Command("systemctl", args...).Output()
return strings.EqualFold(strings.TrimSpace(string(output)), "active")
}

View File

@@ -15,7 +15,6 @@ Depends: ${misc:Depends},
quickshell-git | quickshell,
accountsservice,
cava,
cliphist,
danksearch,
dgop,
matugen,
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
qml6-module-qtquick-layouts,
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct,
wl-clipboard
qt6ct
Provides: dms
Conflicts: dms
Replaces: dms

View File

@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
quickshell | quickshell-git,
accountsservice,
cava,
cliphist,
danksearch,
dgop,
matugen,
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
qml6-module-qtquick-layouts,
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct,
wl-clipboard
qt6ct
Conflicts: dms-git
Replaces: dms-git
Description: DankMaterialShell - Modern Wayland Desktop Shell

View File

@@ -33,7 +33,6 @@ Recommends: cava
Recommends: danksearch
Recommends: matugen
Recommends: quickshell-git
Recommends: wl-clipboard
# Recommended system packages
Recommends: NetworkManager

View File

@@ -24,10 +24,8 @@ Requires: dms-cli = %{version}-%{release}
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct

View File

@@ -11,12 +11,18 @@ let
inherit (config.services.greetd.settings.default_session) user;
compositorPackage =
let
configured = lib.attrByPath [ "programs" cfg.compositor.name "package" ] null config;
in
if configured != null then configured else builtins.getAttr cfg.compositor.name pkgs;
cacheDir = "/var/lib/dms-greeter";
greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
export PATH=$PATH:${
lib.makeBinPath [
cfg.quickshell.package
config.programs.${cfg.compositor.name}.package
compositorPackage
]
}
${
@@ -64,6 +70,7 @@ in
"niri"
"hyprland"
"sway"
"labwc"
];
description = "Compositor to run greeter in";
};

View File

@@ -73,6 +73,13 @@ in
default = hasPluginSettings;
description = ''Whether to manage plugin settings. Automatically enabled if any plugins have settings configured.'';
};
systemd.target = lib.mkOption {
type = lib.types.str;
default = config.wayland.systemd.target;
defaultText = lib.literalExpression "config.wayland.systemd.target";
description = "Systemd target to bind to.";
};
};
config = lib.mkIf cfg.enable {
@@ -84,8 +91,8 @@ in
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
Unit = {
Description = "DankMaterialShell";
PartOf = [ config.wayland.systemd.target ];
After = [ config.wayland.systemd.target ];
PartOf = [ cfg.systemd.target ];
After = [ cfg.systemd.target ];
};
Service = {
@@ -93,7 +100,7 @@ in
Restart = "on-failure";
};
Install.WantedBy = [ config.wayland.systemd.target ];
Install.WantedBy = [ cfg.systemd.target ];
};
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {

View File

@@ -20,15 +20,19 @@ in
imports = [
(import ./options.nix args)
];
options.programs.dank-material-shell.systemd.target = lib.mkOption {
type = lib.types.str;
description = "Systemd target to bind to.";
default = "graphical-session.target";
};
config = lib.mkIf cfg.enable {
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
description = "DankMaterialShell";
path = lib.mkForce [ ];
partOf = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
partOf = [ cfg.systemd.target ];
after = [ cfg.systemd.target ];
wantedBy = [ cfg.systemd.target ];
restartIfChanged = cfg.systemd.restartIfChanged;
serviceConfig = {

View File

@@ -20,12 +20,9 @@ Requires: accountsservice
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: matugen
Recommends: quickshell-git
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct

View File

@@ -23,12 +23,10 @@ Requires: dgop
# Core utilities (Highly recommended for DMS functionality)
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: matugen
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Recommends: wl-clipboard
Suggests: qt6ct
%description

View File

@@ -15,7 +15,6 @@ Depends: ${misc:Depends},
quickshell-git | quickshell,
accountsservice,
cava,
cliphist,
danksearch,
dgop,
matugen,
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
qml6-module-qtquick-layouts,
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct,
wl-clipboard
qt6ct
Provides: dms
Conflicts: dms
Replaces: dms

View File

@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
quickshell | quickshell-git,
accountsservice,
cava,
cliphist,
danksearch,
dgop,
matugen,
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
qml6-module-qtquick-layouts,
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct,
wl-clipboard
qt6ct
Conflicts: dms-git
Replaces: dms-git
Description: DankMaterialShell - Modern Wayland Desktop Shell

View File

@@ -61,11 +61,13 @@
(builtins.substring 6 2 longDate)
];
version =
pkgs.lib.removePrefix "v" (pkgs.lib.trim (builtins.readFile ./quickshell/VERSION))
+ "+date="
+ mkDate (self.lastModifiedDate or "19700101")
+ "_"
+ (self.shortRev or "dirty");
let
rawVersion = pkgs.lib.removePrefix "v" (pkgs.lib.trim (builtins.readFile ./quickshell/VERSION));
cleanVersion = builtins.replaceStrings [ " " ] [ "" ] rawVersion;
dateSuffix = "+date=" + mkDate (self.lastModifiedDate or "19700101");
revSuffix = "_" + (self.shortRev or "dirty");
in
"${cleanVersion}${dateSuffix}${revSuffix}";
in
{
dms-shell = pkgs.buildGoModule (
@@ -83,7 +85,7 @@
ldflags = [
"-s"
"-w"
"-X main.Version=${version}"
"-X 'main.Version=${version}'"
];
nativeBuildInputs = with pkgs; [

View File

@@ -1 +1 @@
Spicy Miso
Saffron Bloom

View File

@@ -45,6 +45,10 @@ Singleton {
Quickshell.execDetached(["cp", strip(from), strip(to)]);
}
function isSteamApp(appId: string): bool {
return appId && /^steam_app_\d+$/.test(appId);
}
function moddedAppId(appId: string): string {
const subs = SettingsData.appIdSubstitutions || [];
for (let i = 0; i < subs.length; i++) {
@@ -60,6 +64,9 @@ Singleton {
}
}
}
const steamMatch = appId.match(/^steam_app_(\d+)$/);
if (steamMatch)
return `steam_icon_${steamMatch[1]}`;
return appId;
}

View File

@@ -82,15 +82,19 @@ Singleton {
popoutOpening();
}
let justClosedSamePopout = false;
let movedFromOtherScreen = false;
for (const otherScreenName in currentPopoutsByScreen) {
if (otherScreenName === screenName)
continue;
const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout)
continue;
if (otherPopout === popout) {
justClosedSamePopout = true;
movedFromOtherScreen = true;
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
}
if (otherPopout.dashVisible !== undefined) {
@@ -112,7 +116,7 @@ Singleton {
}
}
if (currentPopout === popout && popout.shouldBeVisible) {
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
if (popout.dashVisible !== undefined) {
popout.dashVisible = false;
@@ -139,6 +143,7 @@ Singleton {
popout.currentTabIndex = tabIndex;
}
currentPopoutTriggers[screenName] = triggerId;
return;
}
currentPopoutTriggers[screenName] = triggerId;
@@ -153,16 +158,8 @@ Singleton {
ModalManager.closeAllModalsExcept(null);
}
if (justClosedSamePopout) {
Qt.callLater(() => {
if (popout.dashVisible !== undefined) {
popout.dashVisible = true;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true;
} else {
popout.open();
}
});
if (movedFromOtherScreen) {
popout.open();
} else {
if (popout.dashVisible !== undefined) {
popout.dashVisible = true;

View File

@@ -145,6 +145,7 @@ Singleton {
property bool controlCenterShowMicPercent: true
property bool controlCenterShowBatteryIcon: false
property bool controlCenterShowPrinterIcon: false
property bool controlCenterShowScreenSharingIcon: true
property bool showPrivacyButton: true
property bool privacyShowMicIcon: false
property bool privacyShowCameraIcon: false

View File

@@ -904,7 +904,7 @@ Singleton {
if (typeof SettingsData !== "undefined") {
const skipTemplates = [];
if (!SettingsData.runDmsMatugenTemplates) {
skipTemplates.push("gtk", "neovim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode");
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode");
} else {
if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk");

View File

@@ -28,7 +28,8 @@ Singleton {
showMicIcon: false,
showMicPercent: true,
showBatteryIcon: false,
showPrinterIcon: false
showPrinterIcon: false,
showScreenSharingIcon: true
};
leftModel.append(dummy);
centerModel.append(dummy);
@@ -84,6 +85,8 @@ Singleton {
item.showBatteryIcon = order[i].showBatteryIcon;
if (isObj && order[i].showPrinterIcon !== undefined)
item.showPrinterIcon = order[i].showPrinterIcon;
if (isObj && order[i].showScreenSharingIcon !== undefined)
item.showScreenSharingIcon = order[i].showScreenSharingIcon;
model.append(item);
}

View File

@@ -70,6 +70,7 @@ var SPEC = {
controlCenterShowMicPercent: { def: false },
controlCenterShowBatteryIcon: { def: false },
controlCenterShowPrinterIcon: { def: false },
controlCenterShowScreenSharingIcon: { def: true },
showPrivacyButton: { def: true },
privacyShowMicIcon: { def: false },

View File

@@ -203,6 +203,8 @@ Item {
Component.onCompleted: {
dockRecreateDebounce.start();
// Force PolkitService singleton to initialize
PolkitService.polkitAvailable;
}
Connections {
@@ -315,19 +317,44 @@ Item {
}
}
WifiPasswordModal {
id: wifiPasswordModal
LazyLoader {
id: wifiPasswordModalLoader
active: false
Component.onCompleted: {
PopoutService.wifiPasswordModal = wifiPasswordModal;
PopoutService.wifiPasswordModalLoader = wifiPasswordModalLoader;
}
WifiPasswordModal {
id: wifiPasswordModalItem
Component.onCompleted: {
PopoutService.wifiPasswordModal = wifiPasswordModalItem;
}
}
}
PolkitAuthModal {
id: polkitAuthModal
LazyLoader {
id: polkitAuthModalLoader
active: false
Component.onCompleted: {
PopoutService.polkitAuthModal = polkitAuthModal;
PolkitAuthModal {
id: polkitAuthModal
Component.onCompleted: {
PopoutService.polkitAuthModal = polkitAuthModal;
}
}
}
Connections {
target: PolkitService.agent
enabled: PolkitService.polkitAvailable
function onAuthenticationRequestStarted() {
polkitAuthModalLoader.active = true;
if (polkitAuthModalLoader.item)
polkitAuthModalLoader.item.show();
}
}
@@ -349,17 +376,21 @@ Item {
const now = Date.now();
const timeSinceLastPrompt = now - lastCredentialsTime;
if (wifiPasswordModal.visible && timeSinceLastPrompt < 1000) {
wifiPasswordModalLoader.active = true;
if (!wifiPasswordModalLoader.item)
return;
if (wifiPasswordModalLoader.item.visible && timeSinceLastPrompt < 1000) {
NetworkService.cancelCredentials(lastCredentialsToken);
lastCredentialsToken = token;
lastCredentialsTime = now;
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
return;
}
lastCredentialsToken = token;
lastCredentialsTime = now;
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
}
}
@@ -442,17 +473,15 @@ Item {
PopoutService.settingsModalLoader = settingsModalLoader;
}
onActiveChanged: {
if (active && item) {
PopoutService.settingsModal = item;
PopoutService._onSettingsModalLoaded();
}
}
SettingsModal {
id: settingsModal
property bool wasShown: false
Component.onCompleted: {
PopoutService.settingsModal = settingsModal;
PopoutService._onSettingsModalLoaded();
}
onVisibleChanged: {
if (visible) {
wasShown = true;

View File

@@ -132,8 +132,11 @@ Item {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1;
break;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2;
break;
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0;
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0;
@@ -797,11 +800,9 @@ Item {
const modal = PopoutService.settingsModal;
if (modal) {
if (type === "wallpaper") {
modal.wallpaperBrowser.allowStacking = false;
modal.wallpaperBrowser.open();
modal.openWallpaperBrowser(false);
} else if (type === "profile") {
modal.profileBrowser.allowStacking = false;
modal.profileBrowser.open();
modal.openProfileBrowser(false);
}
} else {
PopoutService.openSettings();

View File

@@ -128,7 +128,7 @@ FloatingWindow {
iconName: "open_in_new"
backgroundColor: Theme.surfaceContainerHighest
textColor: Theme.surfaceText
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1.2-release")
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-2-release")
}
DankButton {

View File

@@ -25,7 +25,10 @@ Item {
width: parent.width
totalCount: modal.totalCount
showKeyboardHints: modal.showKeyboardHints
activeTab: modal.activeTab
pinnedCount: modal.pinnedCount
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
onTabChanged: tabName => modal.activeTab = tabName
onClearAllClicked: {
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
modal.clearAll();
@@ -70,18 +73,20 @@ Item {
Rectangle {
width: parent.width
height: parent.height - ClipboardConstants.headerHeight - 70
height: parent.height - y - keyboardHintsContainer.height - Theme.spacingL
radius: Theme.cornerRadius
color: "transparent"
clip: true
// Recents Tab
DankListView {
id: clipboardListView
anchors.fill: parent
model: ScriptModel {
values: clipboardContent.modal.clipboardEntries
values: clipboardContent.modal.unpinnedEntries
objectProp: "id"
}
visible: modal.activeTab === "recents"
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
spacing: Theme.spacingXS
@@ -114,11 +119,11 @@ Item {
}
StyledText {
text: I18n.tr("No clipboard entries found")
text: I18n.tr("No recent clipboard entries found")
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: clipboardContent.modal.clipboardEntries.length === 0
visible: clipboardContent.modal.unpinnedEntries.length === 0
}
delegate: ClipboardEntry {
@@ -135,11 +140,60 @@ Item {
listView: clipboardListView
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
}
}
// Saved Tab
DankListView {
id: savedListView
anchors.fill: parent
model: ScriptModel {
values: clipboardContent.modal.pinnedEntries
objectProp: "id"
}
visible: modal.activeTab === "saved"
spacing: Theme.spacingXS
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
StyledText {
text: I18n.tr("No saved clipboard entries")
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: clipboardContent.modal.pinnedEntries.length === 0
}
delegate: ClipboardEntry {
required property int index
required property var modelData
width: savedListView.width
height: ClipboardConstants.itemHeight
entry: modelData
entryIndex: index + 1
itemIndex: index
isSelected: false
modal: clipboardContent.modal
listView: savedListView
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
}
}
}
Item {
id: keyboardHintsContainer
width: parent.width
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0

View File

@@ -14,6 +14,8 @@ Rectangle {
signal copyRequested
signal deleteRequested
signal pinRequested
signal unpinRequested
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
@@ -50,7 +52,7 @@ Rectangle {
Row {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 68
width: parent.width - 110
spacing: Theme.spacingM
ClipboardThumbnail {
@@ -100,20 +102,32 @@ Rectangle {
}
}
DankActionButton {
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
onClicked: deleteRequested()
spacing: Theme.spacingXS
DankActionButton {
iconName: "push_pin"
iconSize: Theme.iconSize - 6
iconColor: entry.pinned ? Theme.primary : Theme.surfaceText
backgroundColor: entry.pinned ? Theme.primarySelected : "transparent"
onClicked: entry.pinned ? unpinRequested() : pinRequested()
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
onClicked: deleteRequested()
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.rightMargin: 40
anchors.rightMargin: 80
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyRequested()

View File

@@ -8,10 +8,13 @@ Item {
property int totalCount: 0
property bool showKeyboardHints: false
property string activeTab: "recents"
property int pinnedCount: 0
signal keyboardHintsToggled
signal clearAllClicked
signal closeClicked
signal tabChanged(string tabName)
height: ClipboardConstants.headerHeight
@@ -41,6 +44,22 @@ Item {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
iconName: "history"
iconSize: Theme.iconSize - 4
iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText
onClicked: tabChanged("recents")
}
DankActionButton {
iconName: "push_pin"
iconSize: Theme.iconSize - 4
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
opacity: header.pinnedCount > 0 ? 1 : 0
enabled: header.pinnedCount > 0
onClicked: tabChanged("saved")
}
DankActionButton {
iconName: "info"
iconSize: Theme.iconSize - 4

View File

@@ -19,6 +19,8 @@ DankModal {
property int totalCount: 0
property var clipboardEntries: []
property var pinnedEntries: []
property int pinnedCount: 0
property string searchText: ""
property int selectedIndex: 0
property bool keyboardNavigationActive: false
@@ -74,22 +76,37 @@ DankModal {
function updateFilteredModel() {
const query = searchText.trim();
let filtered = [];
if (query.length === 0) {
clipboardEntries = internalEntries;
filtered = internalEntries;
} else {
const lowerQuery = query.toLowerCase();
clipboardEntries = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
filtered = internalEntries.filter(entry =>
entry.preview.toLowerCase().includes(lowerQuery)
);
}
// Sort: pinned first, then by ID descending
filtered.sort((a, b) => {
if (a.pinned !== b.pinned) return b.pinned ? 1 : -1;
return b.id - a.id;
});
clipboardEntries = filtered;
unpinnedEntries = filtered.filter(e => !e.pinned);
totalCount = clipboardEntries.length;
if (clipboardEntries.length === 0) {
if (unpinnedEntries.length === 0) {
keyboardNavigationActive = false;
selectedIndex = 0;
} else if (selectedIndex >= clipboardEntries.length) {
selectedIndex = clipboardEntries.length - 1;
} else if (selectedIndex >= unpinnedEntries.length) {
selectedIndex = unpinnedEntries.length - 1;
}
}
property var internalEntries: []
property var unpinnedEntries: []
property string activeTab: "recents"
function toggle() {
if (shouldBeVisible) {
@@ -135,6 +152,10 @@ DankModal {
return;
}
internalEntries = response.result || [];
pinnedEntries = internalEntries.filter(e => e.pinned);
pinnedCount = pinnedEntries.length;
updateFilteredModel();
});
}
@@ -171,18 +192,87 @@ DankModal {
});
}
function clearAll() {
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
function deletePinnedEntry(entry) {
clearConfirmDialog.show(
I18n.tr("Delete Saved Item?"),
I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."),
function () {
DMSService.sendRequest("clipboard.deleteEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
return;
}
internalEntries = internalEntries.filter(e => e.id !== entry.id);
updateFilteredModel();
ToastService.showInfo(I18n.tr("Saved item deleted"));
});
},
function () {}
);
}
function pinEntry(entry) {
DMSService.sendRequest("clipboard.getPinnedCount", null, function (countResponse) {
if (countResponse.error) {
ToastService.showError(I18n.tr("Failed to check pin limit"));
return;
}
internalEntries = [];
clipboardEntries = [];
totalCount = 0;
const maxPinned = 25; // TODO: Get from config
if (countResponse.result.count >= maxPinned) {
ToastService.showError(I18n.tr("Maximum pinned entries reached") + " (" + maxPinned + ")");
return;
}
DMSService.sendRequest("clipboard.pinEntry", { "id": entry.id }, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to pin entry"));
return;
}
ToastService.showInfo(I18n.tr("Entry pinned"));
refreshClipboard();
});
});
}
function unpinEntry(entry) {
DMSService.sendRequest("clipboard.unpinEntry", { "id": entry.id }, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to unpin entry"));
return;
}
ToastService.showInfo(I18n.tr("Entry unpinned"));
refreshClipboard();
});
}
function clearAll() {
const hasPinned = pinnedCount > 0;
const message = hasPinned
? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount)
: I18n.tr("This will permanently delete all clipboard history.");
clearConfirmDialog.show(
I18n.tr("Clear History?"),
message,
function () {
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
return;
}
refreshClipboard();
if (hasPinned) {
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
}
});
},
function () {}
);
}
function getEntryPreview(entry) {
return entry.preview || "";
}

View File

@@ -11,7 +11,8 @@ FloatingWindow {
id: processListModal
property int currentTab: 0
property var tabNames: ["Processes", "Performance", "System"]
property string searchText: ""
property string expandedPid: ""
property bool shouldHaveFocus: visible
property alias shouldBeVisible: processListModal.visible
@@ -27,9 +28,8 @@ FloatingWindow {
function hide() {
visible = false;
if (processContextMenu.visible) {
if (processContextMenu.visible)
processContextMenu.close();
}
}
function toggle() {
@@ -61,46 +61,39 @@ FloatingWindow {
show();
}
function formatBytes(bytes) {
if (bytes < 1024)
return bytes.toFixed(0) + " B/s";
if (bytes < 1024 * 1024)
return (bytes / 1024).toFixed(1) + " KB/s";
if (bytes < 1024 * 1024 * 1024)
return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
}
objectName: "processListModal"
title: I18n.tr("System Monitor", "sysmon window title")
minimumSize: Qt.size(650, 400)
implicitWidth: 900
implicitHeight: 680
minimumSize: Qt.size(750, 550)
implicitWidth: 1000
implicitHeight: 720
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
if (!visible) {
closingModal();
searchText = "";
expandedPid = "";
DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
} else {
DgopService.addRef(["cpu", "memory", "network", "disk", "system"]);
Qt.callLater(() => {
if (contentFocusScope) {
if (contentFocusScope)
contentFocusScope.forceActiveFocus();
}
});
}
}
Component {
id: processesTabComponent
ProcessesTab {
contextMenu: processContextMenu
}
}
Component {
id: performanceTabComponent
PerformanceTab {}
}
Component {
id: systemTabComponent
SystemTab {}
}
ProcessContextMenu {
id: processContextMenu
}
@@ -128,6 +121,26 @@ FloatingWindow {
currentTab = 2;
event.accepted = true;
return;
case Qt.Key_4:
currentTab = 3;
event.accepted = true;
return;
case Qt.Key_Escape:
if (searchText.length > 0) {
searchText = "";
event.accepted = true;
return;
}
hide();
event.accepted = true;
return;
case Qt.Key_F:
if (event.modifiers & Qt.ControlModifier) {
searchField.forceActiveFocus();
event.accepted = true;
return;
}
break;
}
}
@@ -161,7 +174,7 @@ FloatingWindow {
}
StyledText {
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.", "dgop unavailable error message")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
@@ -171,14 +184,14 @@ FloatingWindow {
}
}
Column {
ColumnLayout {
anchors.fill: parent
spacing: 0
visible: DgopService.dgopAvailable
Item {
width: parent.width
height: 48
Layout.fillWidth: true
Layout.preferredHeight: 48
MouseArea {
anchors.fill: parent
@@ -233,166 +246,276 @@ FloatingWindow {
}
}
Item {
width: parent.width
height: parent.height - 48
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 52
Layout.leftMargin: Theme.spacingL
Layout.rightMargin: Theme.spacingL
spacing: Theme.spacingL
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingL
anchors.topMargin: 0
spacing: Theme.spacingL
Row {
spacing: 2
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineLight
border.width: 1
Repeater {
model: [
{
text: I18n.tr("Processes"),
icon: "list_alt"
},
{
text: I18n.tr("Performance"),
icon: "analytics"
},
{
text: I18n.tr("Disks"),
icon: "storage"
},
{
text: I18n.tr("System"),
icon: "computer"
}
]
Row {
anchors.fill: parent
anchors.margins: 4
spacing: 2
Rectangle {
width: 120
height: 44
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Repeater {
model: tabNames
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
height: 44
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
DankIcon {
name: modelData.icon
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.text
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankIcon {
name: {
const tabIcons = ["list_alt", "analytics", "settings"];
return tabIcons[index] || "tab";
}
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: currentTab = index
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
currentTab = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: 1
Item {
Layout.fillWidth: true
}
Loader {
id: processesTab
DankTextField {
id: searchField
Layout.preferredWidth: 250
Layout.preferredHeight: 40
placeholderText: I18n.tr("Search processes...", "process search placeholder")
leftIconName: "search"
showClearButton: true
text: searchText
visible: currentTab === 0
onTextChanged: searchText = text
}
}
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: Theme.spacingL
Layout.topMargin: Theme.spacingM
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: 1
clip: true
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Loader {
id: processesTabLoader
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 0
visible: currentTab === 0
sourceComponent: ProcessesView {
searchText: processListModal.searchText
expandedPid: processListModal.expandedPid
contextMenu: processContextMenu
onExpandedPidChanged: processListModal.expandedPid = expandedPid
}
}
Loader {
id: performanceTabLoader
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 1
visible: currentTab === 1
sourceComponent: PerformanceView {}
}
Loader {
id: disksTabLoader
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 2
visible: currentTab === 2
sourceComponent: DisksView {}
}
Loader {
id: systemTabLoader
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 3
visible: currentTab === 3
sourceComponent: SystemView {}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 32
Layout.leftMargin: Theme.spacingL
Layout.rightMargin: Theme.spacingL
Layout.bottomMargin: Theme.spacingM
color: "transparent"
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingL
Row {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Processes:", "process count label in footer")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Loader {
id: performanceTab
StyledText {
text: DgopService.processCount.toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Row {
spacing: Theme.spacingXS
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
StyledText {
text: I18n.tr("Uptime:", "uptime label in footer")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Loader {
id: systemTab
StyledText {
text: DgopService.shortUptime || "--"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingL
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "swap_horiz"
size: 14
color: Theme.info
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "storage"
size: 14
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "↓" + formatBytes(DgopService.diskReadRate) + " ↑" + formatBytes(DgopService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "memory"
size: 14
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: DgopService.cpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: DgopService.cpuUsage > 80 ? Theme.error : Theme.surfaceText
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "sd_card"
size: 14
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: DgopService.memoryUsage > 90 ? Theme.error : Theme.surfaceText
}
}
}

View File

@@ -74,9 +74,7 @@ Rectangle {
if (root.parentModal) {
root.parentModal.allowFocusOverride = true;
root.parentModal.shouldHaveFocus = false;
if (root.parentModal.profileBrowser) {
root.parentModal.profileBrowser.open();
}
root.parentModal.openProfileBrowser();
}
}
}

View File

@@ -8,8 +8,26 @@ import qs.Widgets
FloatingWindow {
id: settingsModal
property alias profileBrowser: profileBrowser
property alias wallpaperBrowser: wallpaperBrowser
property var profileBrowser: profileBrowserLoader.item
property var wallpaperBrowser: wallpaperBrowserLoader.item
function openProfileBrowser(allowStacking) {
profileBrowserLoader.active = true;
if (!profileBrowserLoader.item)
return;
if (allowStacking !== undefined)
profileBrowserLoader.item.allowStacking = allowStacking;
profileBrowserLoader.item.open();
}
function openWallpaperBrowser(allowStacking) {
wallpaperBrowserLoader.active = true;
if (!wallpaperBrowserLoader.item)
return;
if (allowStacking !== undefined)
wallpaperBrowserLoader.item.allowStacking = allowStacking;
wallpaperBrowserLoader.item.open();
}
property alias sidebar: sidebar
property int currentTabIndex: 0
property bool shouldHaveFocus: visible
@@ -96,41 +114,51 @@ FloatingWindow {
}
}
FileBrowserModal {
id: profileBrowser
LazyLoader {
id: profileBrowserLoader
active: false
allowStacking: true
parentModal: settingsModal
browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
browserIcon: "person"
browserType: "profile"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
PortalService.setProfileImage(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
FileBrowserModal {
id: profileBrowserItem
allowStacking: true
parentModal: settingsModal
browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
browserIcon: "person"
browserType: "profile"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
PortalService.setProfileImage(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
}
}
}
FileBrowserModal {
id: wallpaperBrowser
LazyLoader {
id: wallpaperBrowserLoader
active: false
allowStacking: true
parentModal: settingsModal
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
SessionData.setWallpaper(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
FileBrowserModal {
id: wallpaperBrowserItem
allowStacking: true
parentModal: settingsModal
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
SessionData.setWallpaper(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
}
}
}

View File

@@ -122,6 +122,21 @@ Rectangle {
contentHeight: audioColumn.height
clip: true
property int maxPinnedInputs: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedInputs() {
const pins = SettingsData.audioInputDevicePins || {}
return normalizePinList(pins["preferredInput"])
}
Column {
id: audioColumn
width: parent.width
@@ -133,16 +148,20 @@ Rectangle {
const nodes = Pipewire.nodes.values.filter(node => {
return node.audio && !node.isSink && !node.isStream;
});
const pins = SettingsData.audioInputDevicePins || {};
const pinnedName = pins["preferredInput"];
const pinnedList = audioContent.getPinnedInputs();
let sorted = [...nodes];
sorted.sort((a, b) => {
// Pinned device first
if (a.name === pinnedName && b.name !== pinnedName)
return -1;
if (b.name === pinnedName && a.name !== pinnedName)
return 1;
const aPinnedIndex = pinnedList.indexOf(a.name)
const bPinnedIndex = pinnedList.indexOf(b.name)
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1)
return 1
if (bPinnedIndex === -1)
return -1
return aPinnedIndex - bPinnedIndex
}
// Then active device
if (a === AudioService.source && b !== AudioService.source)
return -1;
@@ -224,7 +243,7 @@ Rectangle {
height: 28
radius: height / 2
color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
}
@@ -237,7 +256,7 @@ Rectangle {
name: "push_pin"
size: 16
color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
@@ -245,12 +264,12 @@ Rectangle {
StyledText {
text: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
}
font.pixelSize: Theme.fontSizeSmall
color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
@@ -261,16 +280,24 @@ Rectangle {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}));
const isCurrentlyPinned = pins["preferredInput"] === modelData.name;
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}))
let pinnedList = audioContent.normalizePinList(pins["preferredInput"])
const pinIndex = pinnedList.indexOf(modelData.name)
if (isCurrentlyPinned) {
delete pins["preferredInput"];
if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1)
} else {
pins["preferredInput"] = modelData.name;
pinnedList.unshift(modelData.name)
if (pinnedList.length > audioContent.maxPinnedInputs)
pinnedList = pinnedList.slice(0, audioContent.maxPinnedInputs)
}
SettingsData.set("audioInputDevicePins", pins);
if (pinnedList.length > 0)
pins["preferredInput"] = pinnedList
else
delete pins["preferredInput"]
SettingsData.set("audioInputDevicePins", pins)
}
}
}

View File

@@ -132,6 +132,21 @@ Rectangle {
contentHeight: audioColumn.height
clip: true
property int maxPinnedOutputs: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedOutputs() {
const pins = SettingsData.audioOutputDevicePins || {}
return normalizePinList(pins["preferredOutput"])
}
Column {
id: audioColumn
width: parent.width
@@ -143,16 +158,20 @@ Rectangle {
const nodes = Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream;
});
const pins = SettingsData.audioOutputDevicePins || {};
const pinnedName = pins["preferredOutput"];
const pinnedList = audioContent.getPinnedOutputs();
let sorted = [...nodes];
sorted.sort((a, b) => {
// Pinned device first
if (a.name === pinnedName && b.name !== pinnedName)
return -1;
if (b.name === pinnedName && a.name !== pinnedName)
return 1;
const aPinnedIndex = pinnedList.indexOf(a.name)
const bPinnedIndex = pinnedList.indexOf(b.name)
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1)
return 1
if (bPinnedIndex === -1)
return -1
return aPinnedIndex - bPinnedIndex
}
// Then active device
if (a === AudioService.sink && b !== AudioService.sink)
return -1;
@@ -236,7 +255,7 @@ Rectangle {
height: 28
radius: height / 2
color: {
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name;
const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
}
@@ -249,7 +268,7 @@ Rectangle {
name: "push_pin"
size: 16
color: {
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name;
const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
@@ -257,12 +276,12 @@ Rectangle {
StyledText {
text: {
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name;
const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
}
font.pixelSize: Theme.fontSizeSmall
color: {
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name;
const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
@@ -273,16 +292,24 @@ Rectangle {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}));
const isCurrentlyPinned = pins["preferredOutput"] === modelData.name;
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}))
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"])
const pinIndex = pinnedList.indexOf(modelData.name)
if (isCurrentlyPinned) {
delete pins["preferredOutput"];
if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1)
} else {
pins["preferredOutput"] = modelData.name;
pinnedList.unshift(modelData.name)
if (pinnedList.length > audioContent.maxPinnedOutputs)
pinnedList = pinnedList.slice(0, audioContent.maxPinnedOutputs)
}
SettingsData.set("audioOutputDevicePins", pins);
if (pinnedList.length > 0)
pins["preferredOutput"] = pinnedList
else
delete pins["preferredOutput"]
SettingsData.set("audioOutputDevicePins", pins)
}
}
}

View File

@@ -150,6 +150,21 @@ Rectangle {
contentHeight: bluetoothColumn.height
clip: true
property int maxPinnedDevices: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedDevices() {
const pins = SettingsData.bluetoothDevicePins || {}
return normalizePinList(pins["preferredDevice"])
}
Column {
id: bluetoothColumn
width: parent.width
@@ -162,14 +177,18 @@ Rectangle {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return []
const pins = SettingsData.bluetoothDevicePins || {}
const pinnedAddr = pins["preferredDevice"]
const pinnedList = bluetoothContent.getPinnedDevices()
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
devices.sort((a, b) => {
// Pinned device first
if (a.address === pinnedAddr && b.address !== pinnedAddr) return -1
if (b.address === pinnedAddr && a.address !== pinnedAddr) return 1
const aPinnedIndex = pinnedList.indexOf(a.address)
const bPinnedIndex = pinnedList.indexOf(b.address)
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1) return 1
if (bPinnedIndex === -1) return -1
return aPinnedIndex - bPinnedIndex
}
// Then connected devices
if (a.connected && !b.connected) return -1
if (!a.connected && b.connected) return 1
@@ -302,7 +321,7 @@ Rectangle {
height: 28
radius: height / 2
color: {
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
}
@@ -315,7 +334,7 @@ Rectangle {
name: "push_pin"
size: 16
color: {
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
return isThisDevicePinned ? Theme.primary : Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
@@ -323,12 +342,12 @@ Rectangle {
StyledText {
text: {
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin")
}
font.pixelSize: Theme.fontSizeSmall
color: {
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
return isThisDevicePinned ? Theme.primary : Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
@@ -340,14 +359,22 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}))
const isCurrentlyPinned = pins["preferredDevice"] === modelData.address
let pinnedList = bluetoothContent.normalizePinList(pins["preferredDevice"])
const pinIndex = pinnedList.indexOf(modelData.address)
if (isCurrentlyPinned) {
delete pins["preferredDevice"]
if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1)
} else {
pins["preferredDevice"] = modelData.address
pinnedList.unshift(modelData.address)
if (pinnedList.length > bluetoothContent.maxPinnedDevices)
pinnedList = pinnedList.slice(0, bluetoothContent.maxPinnedDevices)
}
if (pinnedList.length > 0)
pins["preferredDevice"] = pinnedList
else
delete pins["preferredDevice"]
SettingsData.set("bluetoothDevicePins", pins)
}
}

View File

@@ -463,20 +463,39 @@ Rectangle {
contentHeight: wifiColumn.height
clip: true
property int maxPinnedNetworks: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedNetworks() {
const pins = SettingsData.wifiNetworkPins || {}
return normalizePinList(pins["preferredWifi"])
}
property var frozenNetworks: []
property bool menuOpen: false
property var sortedNetworks: {
const ssid = NetworkService.currentWifiSSID;
const networks = NetworkService.wifiNetworks;
const pins = SettingsData.wifiNetworkPins || {};
const pinnedSSID = pins["preferredWifi"];
const pinnedList = getPinnedNetworks()
let sorted = [...networks];
sorted.sort((a, b) => {
if (a.ssid === pinnedSSID && b.ssid !== pinnedSSID)
return -1;
if (b.ssid === pinnedSSID && a.ssid !== pinnedSSID)
return 1;
const aPinnedIndex = pinnedList.indexOf(a.ssid)
const bPinnedIndex = pinnedList.indexOf(b.ssid)
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1)
return 1
if (bPinnedIndex === -1)
return -1
return aPinnedIndex - bPinnedIndex
}
if (a.ssid === ssid)
return -1;
if (b.ssid === ssid)
@@ -625,7 +644,7 @@ Rectangle {
height: 28
radius: height / 2
color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
}
@@ -638,7 +657,7 @@ Rectangle {
name: "push_pin"
size: 16
color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
@@ -646,12 +665,12 @@ Rectangle {
StyledText {
text: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? I18n.tr("Pinned") : I18n.tr("Pin");
}
font.pixelSize: Theme.fontSizeSmall
color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
@@ -662,16 +681,24 @@ Rectangle {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
const isCurrentlyPinned = pins["preferredWifi"] === modelData.ssid;
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}))
let pinnedList = wifiContent.normalizePinList(pins["preferredWifi"])
const pinIndex = pinnedList.indexOf(modelData.ssid)
if (isCurrentlyPinned) {
delete pins["preferredWifi"];
if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1)
} else {
pins["preferredWifi"] = modelData.ssid;
pinnedList.unshift(modelData.ssid)
if (pinnedList.length > wifiContent.maxPinnedNetworks)
pinnedList = pinnedList.slice(0, wifiContent.maxPinnedNetworks)
}
SettingsData.set("wifiNetworkPins", pins);
if (pinnedList.length > 0)
pins["preferredWifi"] = pinnedList
else
delete pins["preferredWifi"]
SettingsData.set("wifiNetworkPins", pins)
}
}
}
@@ -687,8 +714,8 @@ Rectangle {
if (modelData.secured && !modelData.saved) {
if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(modelData.ssid);
} else if (PopoutService.wifiPasswordModal) {
PopoutService.wifiPasswordModal.show(modelData.ssid);
} else {
PopoutService.showWifiPasswordModal(modelData.ssid);
}
} else {
NetworkService.connectToWifi(modelData.ssid);
@@ -749,8 +776,8 @@ Rectangle {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(networkContextMenu.currentSSID);
} else if (PopoutService.wifiPasswordModal) {
PopoutService.wifiPasswordModal.show(networkContextMenu.currentSSID);
} else {
PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
}
} else {
NetworkService.connectToWifi(networkContextMenu.currentSSID);

View File

@@ -24,10 +24,12 @@ BasePill {
property bool showMicPercent: widgetData?.showMicPercent !== undefined ? widgetData.showMicPercent : SettingsData.controlCenterShowMicPercent
property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon
property bool showScreenSharingIcon: widgetData?.showScreenSharingIcon !== undefined ? widgetData.showScreenSharingIcon : SettingsData.controlCenterShowScreenSharingIcon
property real touchpadThreshold: 100
property real micAccumulator: 0
property real volumeAccumulator: 0
property real brightnessAccumulator: 0
readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4)
Loader {
active: root.showPrinterIcon
@@ -213,7 +215,25 @@ BasePill {
}
function hasNoVisibleIcons() {
return !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon && !root.showVpnIcon && !root.showBrightnessIcon && !root.showMicIcon && !root.showBatteryIcon && !root.showPrinterIcon;
if (root.showScreenSharingIcon && NiriService.hasCasts)
return false;
if (root.showNetworkIcon && NetworkService.networkAvailable)
return false;
if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected)
return false;
if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled)
return false;
if (root.showAudioIcon)
return false;
if (root.showMicIcon)
return false;
if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice())
return false;
if (root.showBatteryIcon && BatteryService.batteryAvailable)
return false;
if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs())
return false;
return true;
}
content: Component {
@@ -224,48 +244,74 @@ BasePill {
Column {
id: controlColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
width: root.vIconSize
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS
DankIcon {
name: root.getNetworkIconName()
size: Theme.barIconSize(root.barThickness, -4)
color: root.getNetworkIconColor()
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize
visible: root.showScreenSharingIcon && NiriService.hasCasts
DankIcon {
name: "screen_record"
size: root.vIconSize
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
}
Item {
width: root.vIconSize
height: root.vIconSize
visible: root.showNetworkIcon && NetworkService.networkAvailable
DankIcon {
name: root.getNetworkIconName()
size: root.vIconSize
color: root.getNetworkIconColor()
anchors.centerIn: parent
}
}
DankIcon {
name: "vpn_lock"
size: Theme.barIconSize(root.barThickness, -4)
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
DankIcon {
name: "vpn_lock"
size: root.vIconSize
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
}
DankIcon {
name: "bluetooth"
size: Theme.barIconSize(root.barThickness, -4)
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
DankIcon {
name: "bluetooth"
size: root.vIconSize
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
}
Rectangle {
width: audioIconV.implicitWidth + 4
height: audioIconV.implicitHeight + (root.showAudioPercent ? audioPercentV.implicitHeight : 0) + 4
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0)
visible: root.showAudioIcon
DankIcon {
id: audioIconV
name: root.getVolumeIconName()
size: Theme.barIconSize(root.barThickness, -4)
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
@@ -292,21 +338,18 @@ BasePill {
}
}
Rectangle {
width: micIconV.implicitWidth + 4
height: micIconV.implicitHeight + (root.showAudioPercent ? micPercentV.implicitHeight : 0) + 4
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0)
visible: root.showMicIcon
DankIcon {
id: micIconV
name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness, -4)
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
@@ -333,21 +376,18 @@ BasePill {
}
}
Rectangle {
width: brightnessIconV.implicitWidth + 4
height: brightnessIconV.implicitHeight + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight : 0) + 4
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0)
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIconV
name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness, -4)
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
@@ -371,28 +411,43 @@ BasePill {
}
}
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness, -4)
color: root.getBatteryIconColor()
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize
visible: root.showBatteryIcon && BatteryService.batteryAvailable
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: root.vIconSize
color: root.getBatteryIconColor()
anchors.centerIn: parent
}
}
DankIcon {
name: "print"
size: Theme.barIconSize(root.barThickness, -4)
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
DankIcon {
name: "print"
size: root.vIconSize
color: Theme.primary
anchors.centerIn: parent
}
}
DankIcon {
name: "settings"
size: Theme.barIconSize(root.barThickness, -4)
color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: root.vIconSize
height: root.vIconSize
visible: root.hasNoVisibleIcons()
DankIcon {
name: "settings"
size: root.vIconSize
color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.centerIn: parent
}
}
}
@@ -402,6 +457,14 @@ BasePill {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "screen_record"
size: Theme.barIconSize(root.barThickness, -4)
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showScreenSharingIcon && NiriService.hasCasts
}
DankIcon {
id: networkIcon
name: root.getNetworkIconName()

View File

@@ -155,9 +155,17 @@ BasePill {
}
}
DankIcon {
anchors.centerIn: parent
size: 18
name: "sports_esports"
color: Theme.widgetTextColor
visible: root.isVerticalOrientation && activeWindow && activeWindow.appId && appIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
}
Text {
anchors.centerIn: parent
visible: root.isVerticalOrientation && activeWindow && activeWindow.appId && appIcon.status !== Image.Ready
visible: root.isVerticalOrientation && activeWindow && activeWindow.appId && appIcon.status !== Image.Ready && !Paths.isSteamApp(activeWindow.appId)
text: {
if (!activeWindow || !activeWindow.appId)
return "?";

View File

@@ -393,9 +393,19 @@ Item {
}
}
DankIcon {
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"
color: Theme.widgetTextColor
visible: !iconImg.visible && Paths.isSteamApp(appId)
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
visible: !iconImg.visible && !Paths.isSteamApp(appId)
text: {
root._desktopEntriesUpdateTrigger;
if (!appId)
@@ -628,9 +638,19 @@ Item {
}
}
DankIcon {
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"
color: Theme.widgetTextColor
visible: !iconImg.visible && Paths.isSteamApp(appId)
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
visible: !iconImg.visible && !Paths.isSteamApp(appId)
text: {
root._desktopEntriesUpdateTrigger;
if (!appId)

View File

@@ -265,6 +265,7 @@ Item {
if (!byApp[key]) {
const isQuickshell = keyBase === "org.quickshell";
const isSteamApp = Paths.isSteamApp(keyBase);
const moddedId = Paths.moddedAppId(keyBase);
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
const icon = Paths.getAppIcon(keyBase, desktopEntry);
@@ -272,6 +273,7 @@ Item {
"type": "icon",
"icon": icon,
"isQuickshell": isQuickshell,
"isSteamApp": isSteamApp,
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
"count": 1,
"windowId": w.address || w.id,
@@ -1135,7 +1137,7 @@ Item {
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
visible: !modelData.isQuickshell
visible: !modelData.isQuickshell && !modelData.isSteamApp
}
IconImage {
@@ -1151,6 +1153,22 @@ Item {
}
}
IconImage {
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && modelData.icon
}
DankIcon {
anchors.centerIn: parent
size: root.appIconSize
name: "sports_esports"
color: Theme.widgetTextColor
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon
}
MouseArea {
id: rowAppMouseArea
anchors.fill: parent
@@ -1229,7 +1247,7 @@ Item {
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
visible: !modelData.isQuickshell
visible: !modelData.isQuickshell && !modelData.isSteamApp
}
IconImage {
@@ -1245,6 +1263,22 @@ Item {
}
}
IconImage {
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && modelData.icon
}
DankIcon {
anchors.centerIn: parent
size: root.appIconSize
name: "sports_esports"
color: Theme.widgetTextColor
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon
}
MouseArea {
id: colAppMouseArea
anchors.fill: parent

View File

@@ -327,11 +327,12 @@ Item {
clip: false
visible: !_noneAvailable && (!showNoPlayerNow)
ColumnLayout {
x: 72
y: 20
width: 484
height: 370
spacing: Theme.spacingXS
anchors.top: parent.top
anchors.topMargin: 20
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: parent.width

View File

@@ -403,7 +403,7 @@ Item {
width: actualIconSize
height: actualIconSize
anchors.centerIn: parent
visible: iconImg.status !== Image.Ready
visible: iconImg.status !== Image.Ready && appData && appData.appId && !Paths.isSteamApp(appData.appId)
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
@@ -425,6 +425,14 @@ Item {
}
}
DankIcon {
anchors.centerIn: parent
size: actualIconSize
name: "sports_esports"
color: Theme.surfaceText
visible: iconImg.status !== Image.Ready && appData && appData.appId && Paths.isSteamApp(appData.appId)
}
Loader {
anchors.horizontalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.horizontalCenter
anchors.verticalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? parent.verticalCenter : undefined

View File

@@ -2,8 +2,6 @@
set -e
export WLR_DRM_DEVICES=/dev/dri/card1
COMPOSITOR=""
COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter"
@@ -16,7 +14,7 @@ dms-greeter - DankMaterialShell greeter launcher
Usage: dms-greeter --command COMPOSITOR [OPTIONS]
Required:
--command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll or mangowc)
--command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll, mangowc, or labwc)
Options:
-C, --config PATH Custom compositor config file
@@ -33,6 +31,7 @@ Examples:
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
dms-greeter --command mangowc
dms-greeter --command labwc
EOF
}
@@ -231,6 +230,15 @@ SCROLL_EOF
exec scroll -c "$COMPOSITOR_CONFIG"
;;
labwc)
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else
exec labwc --session "$QS_CMD"
fi
;;
mangowc)
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
@@ -241,7 +249,7 @@ SCROLL_EOF
*)
echo "Error: Unsupported compositor: $COMPOSITOR" >&2
echo "Supported compositors: niri, hyprland, sway, mangowc" >&2
echo "Supported compositors: niri, hyprland, sway, scroll, mangowc, labwc" >&2
exit 1
;;
esac

View File

@@ -1406,6 +1406,14 @@ Item {
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.networkAvailable || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio)
DankIcon {
name: "screen_record"
size: Theme.iconSize - 2
color: NiriService.hasActiveCast ? "white" : Qt.rgba(255, 255, 255, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: NiriService.hasCasts
}
DankIcon {
name: {
if (NetworkService.wifiToggling)

View File

@@ -262,9 +262,7 @@ DankOSD {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() {
if (!vertSlider.dragging) {
vertSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100));
}
vertSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100));
}
}
}

View File

@@ -0,0 +1,346 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
function formatSpeed(bytesPerSec) {
if (bytesPerSec < 1024)
return bytesPerSec.toFixed(0) + " B/s";
if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
if (bytesPerSec < 1024 * 1024 * 1024)
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
}
Component.onCompleted: {
DgopService.addRef(["disk", "diskmounts"]);
}
Component.onDestruction: {
DgopService.removeRef(["disk", "diskmounts"]);
}
ColumnLayout {
anchors.fill: parent
spacing: Theme.spacingM
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
RowLayout {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXL
Column {
Layout.fillWidth: true
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
DankIcon {
name: "storage"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Disk I/O", "disk io header in system monitor")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingL
Row {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Read:", "disk read label")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: root.formatSpeed(DgopService.diskReadRate)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.primary
}
}
Row {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Write:", "disk write label")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: root.formatSpeed(DgopService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.warning
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
spacing: Theme.spacingS
DankIcon {
name: "folder"
size: Theme.iconSize - 2
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Mount Points", "mount points header in system monitor")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Theme.outlineLight
}
DankListView {
id: mountListView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 4
model: DgopService.diskMounts
delegate: Rectangle {
required property var modelData
required property int index
width: mountListView.width
height: 60
radius: Theme.cornerRadius
color: mountMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
readonly property real usedPct: {
const pctStr = modelData?.percent ?? "0%";
return parseFloat(pctStr.replace("%", "")) / 100;
}
MouseArea {
id: mountMouseArea
anchors.fill: parent
hoverEnabled: true
}
RowLayout {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingM
Column {
Layout.fillWidth: true
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
DankIcon {
name: {
const mp = modelData?.mount ?? "";
if (mp === "/")
return "home";
if (mp === "/home")
return "person";
if (mp.includes("boot"))
return "memory";
if (mp.includes("media") || mp.includes("mnt"))
return "usb";
return "folder";
}
size: Theme.iconSize - 4
color: Theme.surfaceText
opacity: 0.8
}
StyledText {
text: modelData?.mount ?? ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
}
}
Row {
spacing: Theme.spacingS
StyledText {
text: modelData?.device ?? ""
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.surfaceVariantText
}
StyledText {
text: modelData?.fstype ?? ""
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
}
}
}
Column {
Layout.preferredWidth: 200
spacing: Theme.spacingXS
Rectangle {
width: parent.width
height: 8
radius: 4
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: parent.width * Math.min(1, parent.parent.parent.parent.usedPct)
height: parent.height
radius: 4
color: {
const pct = parent.parent.parent.parent.usedPct;
if (pct > 0.95)
return Theme.error;
if (pct > 0.85)
return Theme.warning;
return Theme.primary;
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
}
Row {
anchors.right: parent.right
spacing: Theme.spacingS
StyledText {
text: modelData?.used ?? ""
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
StyledText {
text: "/"
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.surfaceVariantText
}
StyledText {
text: modelData?.size ?? ""
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
}
}
}
StyledText {
Layout.preferredWidth: 50
text: modelData?.percent ?? ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
const pct = parent.parent.usedPct;
if (pct > 0.95)
return Theme.error;
if (pct > 0.85)
return Theme.warning;
return Theme.surfaceText;
}
horizontalAlignment: Text.AlignRight
}
}
}
Rectangle {
anchors.centerIn: parent
width: 300
height: 80
radius: Theme.cornerRadius
color: "transparent"
visible: DgopService.diskMounts.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: "storage"
size: 32
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No mount points found", "empty state in disk mounts list")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}
}

View File

@@ -1,451 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Column {
function formatNetworkSpeed(bytesPerSec) {
if (bytesPerSec < 1024) {
return bytesPerSec.toFixed(0) + " B/s";
} else if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
}
}
function formatDiskSpeed(bytesPerSec) {
if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
}
}
anchors.fill: parent
spacing: Theme.spacingM
Component.onCompleted: {
DgopService.addRef(["cpu", "memory", "network", "disk"]);
}
Component.onDestruction: {
DgopService.removeRef(["cpu", "memory", "network", "disk"]);
}
Rectangle {
width: parent.width
height: 200
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
height: 32
spacing: Theme.spacingM
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 80
height: 24
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: `${DgopService.cpuUsage.toFixed(1)}%`
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
anchors.centerIn: parent
}
}
Item {
width: parent.width - 280
height: 1
}
StyledText {
text: `${DgopService.cpuCores} cores`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
DankFlickable {
clip: true
width: parent.width
height: parent.height - 40
contentHeight: coreUsageColumn.implicitHeight
Column {
id: coreUsageColumn
width: parent.width
spacing: 6
Repeater {
model: DgopService.perCoreCpuUsage
Row {
width: parent.width
height: 20
spacing: Theme.spacingS
StyledText {
text: `C${index}`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 24
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: parent.width - 80
height: 6
radius: 3
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: parent.width * Math.min(1, modelData / 100)
height: parent.height
radius: parent.radius
color: {
const usage = modelData;
if (usage > 80) {
return Theme.error;
}
if (usage > 60) {
return Theme.warning;
}
return Theme.primary;
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
}
StyledText {
text: modelData ? `${modelData.toFixed(0)}%` : "0%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: 32
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
Row {
width: parent.width
height: 80
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: I18n.tr("Memory")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: `${DgopService.formatSystemMemory(DgopService.usedMemoryKB)} / ${DgopService.formatSystemMemory(DgopService.totalMemoryKB)}`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 120
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: DgopService.totalMemoryKB > 0 ? parent.width * (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0
height: parent.height
radius: parent.radius
color: {
const usage = DgopService.totalMemoryKB > 0 ? (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0;
if (usage > 0.9) {
return Theme.error;
}
if (usage > 0.7) {
return Theme.warning;
}
return Theme.secondary;
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: DgopService.totalMemoryKB > 0 ? `${((DgopService.usedMemoryKB / DgopService.totalMemoryKB) * 100).toFixed(1)}% used` : "No data"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: I18n.tr("Swap")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: DgopService.totalSwapKB > 0 ? `${DgopService.formatSystemMemory(DgopService.usedSwapKB)} / ${DgopService.formatSystemMemory(DgopService.totalSwapKB)}` : "No swap"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 120
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: DgopService.totalSwapKB > 0 ? parent.width * (DgopService.usedSwapKB / DgopService.totalSwapKB) : 0
height: parent.height
radius: parent.radius
color: {
if (!DgopService.totalSwapKB) {
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3);
}
const usage = DgopService.usedSwapKB / DgopService.totalSwapKB;
if (usage > 0.9) {
return Theme.error;
}
if (usage > 0.7) {
return Theme.warning;
}
return Theme.info;
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: DgopService.totalSwapKB > 0 ? `${((DgopService.usedSwapKB / DgopService.totalSwapKB) * 100).toFixed(1)}% used` : "N/A"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
Row {
width: parent.width
height: 80
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Network")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "↓"
font.pixelSize: Theme.fontSizeSmall
color: Theme.info
}
StyledText {
text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "↑"
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
}
StyledText {
text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Disk")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "R"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
StyledText {
text: formatDiskSpeed(DgopService.diskReadRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
}
StyledText {
text: formatDiskSpeed(DgopService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
}
}

View File

@@ -0,0 +1,304 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
readonly property int historySize: 60
property var cpuHistory: []
property var memoryHistory: []
property var networkRxHistory: []
property var networkTxHistory: []
property var diskReadHistory: []
property var diskWriteHistory: []
function formatBytes(bytes) {
if (bytes < 1024)
return bytes.toFixed(0) + " B/s";
if (bytes < 1024 * 1024)
return (bytes / 1024).toFixed(1) + " KB/s";
if (bytes < 1024 * 1024 * 1024)
return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
}
function addToHistory(arr, val) {
const newArr = arr.slice();
newArr.push(val);
if (newArr.length > historySize)
newArr.shift();
return newArr;
}
function sampleData() {
cpuHistory = addToHistory(cpuHistory, DgopService.cpuUsage);
memoryHistory = addToHistory(memoryHistory, DgopService.memoryUsage);
networkRxHistory = addToHistory(networkRxHistory, DgopService.networkRxRate);
networkTxHistory = addToHistory(networkTxHistory, DgopService.networkTxRate);
diskReadHistory = addToHistory(diskReadHistory, DgopService.diskReadRate);
diskWriteHistory = addToHistory(diskWriteHistory, DgopService.diskWriteRate);
}
Component.onCompleted: {
DgopService.addRef(["cpu", "memory", "network", "disk", "diskmounts", "system"]);
}
Component.onDestruction: {
DgopService.removeRef(["cpu", "memory", "network", "disk", "diskmounts", "system"]);
}
SystemClock {
id: sampleClock
precision: SystemClock.Seconds
onDateChanged: {
if (date.getSeconds() % 1 === 0)
root.sampleData();
}
}
ColumnLayout {
anchors.fill: parent
spacing: Theme.spacingM
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: (root.height - Theme.spacingM * 2) / 2
spacing: Theme.spacingM
PerformanceCard {
Layout.fillWidth: true
Layout.fillHeight: true
title: "CPU"
icon: "memory"
value: DgopService.cpuUsage.toFixed(1) + "%"
subtitle: DgopService.cpuModel || (DgopService.cpuCores + " cores")
accentColor: Theme.primary
history: root.cpuHistory
maxValue: 100
showSecondary: false
extraInfo: DgopService.cpuTemperature > 0 ? (DgopService.cpuTemperature.toFixed(0) + "°C") : ""
extraInfoColor: DgopService.cpuTemperature > 80 ? Theme.error : (DgopService.cpuTemperature > 60 ? Theme.warning : Theme.surfaceVariantText)
}
PerformanceCard {
Layout.fillWidth: true
Layout.fillHeight: true
title: I18n.tr("Memory")
icon: "sd_card"
value: DgopService.memoryUsage.toFixed(1) + "%"
subtitle: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
accentColor: Theme.secondary
history: root.memoryHistory
maxValue: 100
showSecondary: false
extraInfo: DgopService.totalSwapKB > 0 ? ("Swap: " + DgopService.formatSystemMemory(DgopService.usedSwapKB)) : ""
extraInfoColor: Theme.surfaceVariantText
}
}
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: (root.height - Theme.spacingM * 2) / 2
spacing: Theme.spacingM
PerformanceCard {
Layout.fillWidth: true
Layout.fillHeight: true
title: I18n.tr("Network")
icon: "swap_horiz"
value: "↓ " + root.formatBytes(DgopService.networkRxRate)
subtitle: "↑ " + root.formatBytes(DgopService.networkTxRate)
accentColor: Theme.info
history: root.networkRxHistory
history2: root.networkTxHistory
maxValue: 0
showSecondary: true
extraInfo: ""
extraInfoColor: Theme.surfaceVariantText
}
PerformanceCard {
Layout.fillWidth: true
Layout.fillHeight: true
title: I18n.tr("Disk")
icon: "storage"
value: "R: " + root.formatBytes(DgopService.diskReadRate)
subtitle: "W: " + root.formatBytes(DgopService.diskWriteRate)
accentColor: Theme.warning
history: root.diskReadHistory
history2: root.diskWriteHistory
maxValue: 0
showSecondary: true
extraInfo: {
const rootMount = DgopService.diskMounts.find(m => m.mountpoint === "/");
if (rootMount) {
const usedPct = ((rootMount.used || 0) / Math.max(1, rootMount.total || 1) * 100).toFixed(0);
return "/ " + usedPct + "% used";
}
return "";
}
extraInfoColor: Theme.surfaceVariantText
}
}
}
component PerformanceCard: Rectangle {
id: card
property string title: ""
property string icon: ""
property string value: ""
property string subtitle: ""
property color accentColor: Theme.primary
property var history: []
property var history2: null
property real maxValue: 100
property bool showSecondary: false
property string extraInfo: ""
property color extraInfoColor: Theme.surfaceVariantText
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: 1
Canvas {
id: graphCanvas
anchors.fill: parent
anchors.margins: 4
renderStrategy: Canvas.Cooperative
property var hist: card.history
property var hist2: card.history2
onHistChanged: requestPaint()
onHist2Changed: requestPaint()
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.clearRect(0, 0, width, height);
if (!hist || hist.length < 2)
return;
let max = card.maxValue;
if (max <= 0) {
max = 1;
for (let k = 0; k < hist.length; k++)
max = Math.max(max, hist[k]);
if (hist2) {
for (let l = 0; l < hist2.length; l++)
max = Math.max(max, hist2[l]);
}
max *= 1.1;
}
const c = card.accentColor;
const grad = ctx.createLinearGradient(0, 0, 0, height);
grad.addColorStop(0, Qt.rgba(c.r, c.g, c.b, 0.25));
grad.addColorStop(1, Qt.rgba(c.r, c.g, c.b, 0.02));
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, height);
for (let i = 0; i < hist.length; i++) {
const x = (width / (root.historySize - 1)) * i;
const y = height - (hist[i] / max) * height * 0.8;
ctx.lineTo(x, y);
}
ctx.lineTo((width / (root.historySize - 1)) * (hist.length - 1), height);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.8);
ctx.lineWidth = 2;
ctx.beginPath();
for (let j = 0; j < hist.length; j++) {
const px = (width / (root.historySize - 1)) * j;
const py = height - (hist[j] / max) * height * 0.8;
j === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.stroke();
if (hist2 && hist2.length >= 2 && card.showSecondary) {
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.4);
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
for (let m = 0; m < hist2.length; m++) {
const sx = (width / (root.historySize - 1)) * m;
const sy = height - (hist2[m] / max) * height * 0.8;
m === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
}
ctx.stroke();
ctx.setLineDash([]);
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingS
DankIcon {
name: card.icon
size: Theme.iconSize
color: card.accentColor
}
StyledText {
text: card.title
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
Item {
Layout.fillWidth: true
}
StyledText {
text: card.extraInfo
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: card.extraInfoColor
visible: card.extraInfo.length > 0
}
}
Item {
Layout.fillHeight: true
}
StyledText {
text: card.value
font.pixelSize: Theme.fontSizeXLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: card.subtitle
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}
}

View File

@@ -19,13 +19,10 @@ Popup {
const menuWidth = processContextMenu.width;
const menuHeight = processContextMenu.height;
if (finalX + menuWidth > parentWidth) {
if (finalX + menuWidth > parentWidth)
finalX = Math.max(0, parentWidth - menuWidth);
}
if (finalY + menuHeight > parentHeight) {
if (finalY + menuHeight > parentHeight)
finalY = Math.max(0, parentHeight - menuHeight);
}
}
processContextMenu.x = finalX;
@@ -33,25 +30,19 @@ Popup {
open();
}
width: 180
width: 200
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape;
}
onOpened: {
outsideClickTimer.start();
}
onClosed: closePolicy = Popup.CloseOnEscape
onOpened: outsideClickTimer.start()
Timer {
id: outsideClickTimer
interval: 100
onTriggered: {
processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside;
}
onTriggered: processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
}
background: Rectangle {
@@ -59,8 +50,6 @@ Popup {
}
contentItem: Rectangle {
id: menuContent
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
@@ -68,157 +57,140 @@ Popup {
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Copy PID")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyPidArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData) {
Quickshell.execDetached(["dms", "cl", "copy", processContextMenu.processData.pid.toString()]);
}
processContextMenu.close();
}
MenuItem {
text: I18n.tr("Copy PID")
iconName: "tag"
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["dms", "cl", "copy", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: copyNameArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Copy Process Name")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyNameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData) {
const processName = processContextMenu.processData.displayName || processContextMenu.processData.command;
Quickshell.execDetached(["dms", "cl", "copy", processName]);
}
processContextMenu.close();
MenuItem {
text: I18n.tr("Copy Name")
iconName: "content_copy"
onClicked: {
if (processContextMenu.processData) {
const name = processContextMenu.processData.command || "";
Quickshell.execDetached(["dms", "cl", "copy", name]);
}
processContextMenu.close();
}
}
MenuItem {
text: I18n.tr("Copy Full Command")
iconName: "code"
onClicked: {
if (processContextMenu.processData) {
const fullCmd = processContextMenu.processData.fullCommand || processContextMenu.processData.command || "";
Quickshell.execDetached(["dms", "cl", "copy", fullCmd]);
}
processContextMenu.close();
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15)
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
MenuItem {
text: I18n.tr("Kill Process")
iconName: "close"
dangerous: true
enabled: processContextMenu.processData
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Kill Process")
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (killArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: killArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData) {
Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]);
}
processContextMenu.close();
}
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
MenuItem {
text: I18n.tr("Force Kill (SIGKILL)")
iconName: "dangerous"
dangerous: true
enabled: processContextMenu.processData && processContextMenu.processData.pid > 1000
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Force Kill Process")
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (forceKillArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: forceKillArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData) {
Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]);
}
processContextMenu.close();
}
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
}
}
component MenuItem: Rectangle {
id: menuItem
property string text: ""
property string iconName: ""
property bool dangerous: false
property bool enabled: true
signal clicked
width: parent.width
height: 32
radius: Theme.cornerRadius
color: {
if (!enabled)
return "transparent";
if (dangerous)
return menuItemArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent";
return menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
}
opacity: enabled ? 1 : 0.5
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: menuItem.iconName
size: 16
color: {
if (!menuItem.enabled)
return Theme.surfaceVariantText;
if (menuItem.dangerous && menuItemArea.containsMouse)
return Theme.error;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: menuItem.text
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Normal
color: {
if (!menuItem.enabled)
return Theme.surfaceVariantText;
if (menuItem.dangerous && menuItemArea.containsMouse)
return Theme.error;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: menuItemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: menuItem.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: menuItem.enabled
onClicked: menuItem.clicked()
}
}
}

View File

@@ -1,200 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: processItem
property var process: null
property var contextMenu: null
width: parent ? parent.width : 0
height: 40
radius: Theme.cornerRadius
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.primary, 0)
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
border.width: 1
MouseArea {
id: processMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process;
const globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y);
const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
contextMenu.show(localPos.x, localPos.y);
}
}
}
onPressAndHold: {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process;
const globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2);
contextMenu.show(globalPos.x, globalPos.y);
}
}
}
Item {
anchors.fill: parent
anchors.margins: 8
DankIcon {
id: processIcon
name: DgopService.getProcessIcon(process ? process.command : "")
size: Theme.iconSize - 4
color: {
if (process && process.cpu > 80) {
return Theme.error;
}
return Theme.surfaceText;
}
opacity: 0.8
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: process ? process.displayName : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: 250
elide: Text.ElideRight
anchors.left: processIcon.right
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: cpuBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.cpu > 80) {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
}
anchors.right: parent.right
anchors.rightMargin: 194
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: DgopService.formatCpuUsage(process ? process.cpu : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.cpu > 80) {
return Theme.error;
}
return Theme.surfaceText;
}
anchors.centerIn: parent
}
}
Rectangle {
id: memoryBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.memoryKB > 1024 * 1024) {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
}
anchors.right: parent.right
anchors.rightMargin: 102
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: DgopService.formatMemoryUsage(process ? process.memoryKB : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.memoryKB > 1024 * 1024) {
return Theme.error;
}
return Theme.surfaceText;
}
anchors.centerIn: parent
}
}
StyledText {
text: process ? process.pid.toString() : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
width: 50
horizontalAlignment: Text.AlignRight
anchors.right: parent.right
anchors.rightMargin: 40
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: menuButton
width: 28
height: 28
radius: Theme.cornerRadius
color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: menuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process;
const globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height);
const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
contextMenu.show(localPos.x, localPos.y);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}

View File

@@ -12,32 +12,39 @@ DankPopout {
property var parentWidget: null
property var triggerScreen: null
property string searchText: ""
property string expandedPid: ""
function hide() {
close();
if (processContextMenu.visible) {
if (processContextMenu.visible)
processContextMenu.close();
}
}
function show() {
open();
}
popupWidth: 600
popupHeight: 600
popupWidth: 650
popupHeight: 550
triggerWidth: 55
positioning: ""
screen: triggerScreen
shouldBeVisible: false
onBackgroundClicked: {
if (processContextMenu.visible) {
if (processContextMenu.visible)
processContextMenu.close();
}
close();
}
onShouldBeVisibleChanged: {
if (!shouldBeVisible) {
searchText = "";
expandedPid = "";
}
}
Ref {
service: DgopService
}
@@ -55,55 +62,247 @@ DankPopout {
radius: Theme.cornerRadius
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
clip: true
antialiasing: true
smooth: true
focus: true
Component.onCompleted: {
if (processListPopout.shouldBeVisible) {
if (processListPopout.shouldBeVisible)
forceActiveFocus();
}
processContextMenu.parent = processListContent;
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
switch (event.key) {
case Qt.Key_Escape:
if (processListPopout.searchText.length > 0) {
processListPopout.searchText = "";
event.accepted = true;
return;
}
processListPopout.close();
event.accepted = true;
return;
case Qt.Key_F:
if (event.modifiers & Qt.ControlModifier) {
searchField.forceActiveFocus();
event.accepted = true;
return;
}
break;
}
}
Connections {
target: processListPopout
function onShouldBeVisibleChanged() {
if (processListPopout.shouldBeVisible) {
Qt.callLater(() => {
processListContent.forceActiveFocus();
});
Qt.callLater(() => processListContent.forceActiveFocus());
}
}
target: processListPopout
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
Rectangle {
RowLayout {
Layout.fillWidth: true
height: systemOverview.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
spacing: Theme.spacingM
SystemOverview {
id: systemOverview
Row {
spacing: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
DankIcon {
name: "analytics"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Processes")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
Layout.fillWidth: true
}
DankTextField {
id: searchField
Layout.preferredWidth: Theme.fontSizeMedium * 14
Layout.preferredHeight: Theme.fontSizeMedium * 2.5
placeholderText: I18n.tr("Search...")
leftIconName: "search"
showClearButton: true
text: processListPopout.searchText
onTextChanged: processListPopout.searchText = text
}
}
Item {
id: statsContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.max(leftInfo.height, gaugesRow.height) + Theme.spacingS
function compactMem(kb) {
if (kb < 1024 * 1024) {
const mb = kb / 1024;
return mb >= 100 ? mb.toFixed(0) + " MB" : mb.toFixed(1) + " MB";
}
const gb = kb / (1024 * 1024);
return gb >= 10 ? gb.toFixed(0) + " GB" : gb.toFixed(1) + " GB";
}
readonly property real gaugeSize: Theme.fontSizeMedium * 6.5
readonly property var enabledGpusWithTemp: {
if (!SessionData.enabledGpuPciIds || SessionData.enabledGpuPciIds.length === 0)
return [];
const result = [];
for (const gpu of DgopService.availableGpus) {
if (SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 && gpu.temperature > 0)
result.push(gpu);
}
return result;
}
readonly property bool hasGpu: enabledGpusWithTemp.length > 0
Row {
id: leftInfo
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Theme.fontSizeMedium * 3
height: width
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
SystemLogo {
anchors.centerIn: parent
width: parent.width * 0.7
height: width
colorOverride: Theme.primary
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS / 2
StyledText {
text: DgopService.hostname || "localhost"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: DgopService.distribution || "Linux"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Row {
spacing: Theme.spacingS
Row {
spacing: Theme.spacingXS
DankIcon {
name: "schedule"
size: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: DgopService.shortUptime || "--"
font.pixelSize: Theme.fontSizeSmall - 1
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
}
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
StyledText {
text: DgopService.processCount + " " + I18n.tr("procs", "short for processes")
font.pixelSize: Theme.fontSizeSmall - 1
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
}
}
}
}
Row {
id: gaugesRow
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
CircleGauge {
width: statsContainer.gaugeSize
height: statsContainer.gaugeSize
value: DgopService.cpuUsage / 100
label: DgopService.cpuUsage.toFixed(0) + "%"
sublabel: "CPU"
detail: DgopService.cpuTemperature > 0 ? (DgopService.cpuTemperature.toFixed(0) + "°") : ""
accentColor: DgopService.cpuUsage > 80 ? Theme.error : (DgopService.cpuUsage > 50 ? Theme.warning : Theme.primary)
detailColor: DgopService.cpuTemperature > 85 ? Theme.error : (DgopService.cpuTemperature > 70 ? Theme.warning : Theme.surfaceVariantText)
}
CircleGauge {
width: statsContainer.gaugeSize
height: statsContainer.gaugeSize
value: DgopService.memoryUsage / 100
label: statsContainer.compactMem(DgopService.usedMemoryKB)
sublabel: I18n.tr("Memory")
detail: DgopService.totalSwapKB > 0 ? ("+" + statsContainer.compactMem(DgopService.usedSwapKB)) : ""
accentColor: DgopService.memoryUsage > 90 ? Theme.error : (DgopService.memoryUsage > 70 ? Theme.warning : Theme.secondary)
}
CircleGauge {
width: statsContainer.gaugeSize
height: statsContainer.gaugeSize
visible: statsContainer.hasGpu
readonly property var gpu: statsContainer.enabledGpusWithTemp[0] ?? null
readonly property color vendorColor: {
const vendor = (gpu?.vendor ?? "").toLowerCase();
if (vendor.includes("nvidia"))
return Theme.success;
if (vendor.includes("amd"))
return Theme.error;
if (vendor.includes("intel"))
return Theme.info;
return Theme.info;
}
value: Math.min(1, (gpu?.temperature ?? 0) / 100)
label: (gpu?.temperature ?? 0) > 0 ? ((gpu?.temperature ?? 0).toFixed(0) + "°C") : "--"
sublabel: "GPU"
accentColor: {
const temp = gpu?.temperature ?? 0;
if (temp > 85)
return Theme.error;
if (temp > 70)
return Theme.warning;
return vendorColor;
}
}
}
}
@@ -112,16 +311,176 @@ DankPopout {
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 0
clip: true
ProcessListView {
ProcessesView {
anchors.fill: parent
anchors.margins: Theme.spacingS
searchText: processListPopout.searchText
expandedPid: processListPopout.expandedPid
contextMenu: processContextMenu
onExpandedPidChanged: processListPopout.expandedPid = expandedPid
}
}
}
}
}
component CircleGauge: Item {
id: gaugeRoot
property real value: 0
property string label: ""
property string sublabel: ""
property string detail: ""
property color accentColor: Theme.primary
property color detailColor: Theme.surfaceVariantText
readonly property real thickness: Math.max(4, Math.min(width, height) / 15)
readonly property real glowExtra: thickness * 1.4
readonly property real arcPadding: thickness / 1.3
readonly property real innerDiameter: width - (arcPadding + thickness + glowExtra) * 2
readonly property real maxTextWidth: innerDiameter * 0.9
readonly property real baseLabelSize: Math.round(width * 0.18)
readonly property real labelSize: Math.round(Math.min(baseLabelSize, maxTextWidth / Math.max(1, label.length * 0.65)))
readonly property real sublabelSize: Math.round(Math.min(width * 0.13, maxTextWidth / Math.max(1, sublabel.length * 0.7)))
readonly property real detailSize: Math.round(Math.min(width * 0.12, maxTextWidth / Math.max(1, detail.length * 0.65)))
property real animValue: 0
onValueChanged: animValue = Math.min(1, Math.max(0, value))
Behavior on animValue {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
Component.onCompleted: animValue = Math.min(1, Math.max(0, value))
Canvas {
id: glowCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
const startAngle = -Math.PI * 0.5;
const endAngle = Math.PI * 1.5;
ctx.lineCap = "round";
if (gaugeRoot.animValue > 0) {
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, prog);
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.2);
ctx.lineWidth = gaugeRoot.thickness + gaugeRoot.glowExtra;
ctx.stroke();
}
}
Connections {
target: gaugeRoot
function onAnimValueChanged() {
glowCanvas.requestPaint();
}
function onAccentColorChanged() {
glowCanvas.requestPaint();
}
function onWidthChanged() {
glowCanvas.requestPaint();
}
function onHeightChanged() {
glowCanvas.requestPaint();
}
}
Component.onCompleted: requestPaint()
}
Canvas {
id: arcCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
const startAngle = -Math.PI * 0.5;
const endAngle = Math.PI * 1.5;
ctx.lineCap = "round";
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.1);
ctx.lineWidth = gaugeRoot.thickness;
ctx.stroke();
if (gaugeRoot.animValue > 0) {
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, prog);
ctx.strokeStyle = gaugeRoot.accentColor;
ctx.lineWidth = gaugeRoot.thickness;
ctx.stroke();
}
}
Connections {
target: gaugeRoot
function onAnimValueChanged() {
arcCanvas.requestPaint();
}
function onAccentColorChanged() {
arcCanvas.requestPaint();
}
function onWidthChanged() {
arcCanvas.requestPaint();
}
function onHeightChanged() {
arcCanvas.requestPaint();
}
}
Component.onCompleted: requestPaint()
}
Column {
anchors.centerIn: parent
spacing: 1
StyledText {
text: gaugeRoot.label
font.pixelSize: gaugeRoot.labelSize
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: gaugeRoot.sublabel
font.pixelSize: gaugeRoot.sublabelSize
font.weight: Font.Medium
color: gaugeRoot.accentColor
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: gaugeRoot.detail
font.pixelSize: gaugeRoot.detailSize
font.family: SettingsData.monoFontFamily
color: gaugeRoot.detailColor
anchors.horizontalCenter: parent.horizontalCenter
visible: gaugeRoot.detail.length > 0
}
}
}
}

View File

@@ -1,264 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property var contextMenu: null
Component.onCompleted: {
DgopService.addRef(["processes"]);
}
Component.onDestruction: {
DgopService.removeRef(["processes"]);
}
Item {
id: columnHeaders
width: parent.width
anchors.leftMargin: 8
height: 24
Rectangle {
width: 60
height: 20
color: {
if (DgopService.currentSort === "name") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
}
return processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
}
radius: Theme.cornerRadius
anchors.left: parent.left
anchors.leftMargin: 0
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Process")
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "name" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "name" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: processHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("name");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 80
height: 20
color: {
if (DgopService.currentSort === "cpu") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
}
return cpuHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
}
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 200
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "cpu" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "cpu" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: cpuHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("cpu");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 80
height: 20
color: {
if (DgopService.currentSort === "memory") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
}
return memoryHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
}
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 112
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "RAM"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "memory" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "memory" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: memoryHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("memory");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 50
height: 20
color: {
if (DgopService.currentSort === "pid") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
}
return pidHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
}
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 53
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "PID"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "pid" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "pid" ? 1 : 0.7
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
MouseArea {
id: pidHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("pid");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0)
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: DgopService.sortDescending ? "↓" : "↑"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
// TODO: Re-implement sort order toggle
id: sortOrderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
DankListView {
id: processListView
property string keyRoleName: "pid"
width: parent.width
height: parent.height - columnHeaders.height
clip: true
spacing: 4
model: ScriptModel {
values: DgopService.processes
objectProp: "pid"
}
delegate: ProcessListItem {
process: modelData
contextMenu: root.contextMenu
}
}
}

View File

@@ -1,28 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Modules.ProcessList
import qs.Services
ColumnLayout {
id: processesTab
property var contextMenu: null
anchors.fill: parent
spacing: Theme.spacingM
SystemOverview {
Layout.fillWidth: true
}
ProcessListView {
Layout.fillWidth: true
Layout.fillHeight: true
contextMenu: processesTab.contextMenu
}
ProcessContextMenu {
id: localContextMenu
}
}

View File

@@ -0,0 +1,607 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property string searchText: ""
property string expandedPid: ""
property var contextMenu: null
property bool hoveringExpandedItem: false
readonly property bool pauseUpdates: hoveringExpandedItem || (contextMenu?.visible ?? false)
property var cachedProcesses: []
onFilteredProcessesChanged: {
if (!pauseUpdates)
cachedProcesses = filteredProcesses;
}
onPauseUpdatesChanged: {
if (!pauseUpdates)
cachedProcesses = filteredProcesses;
}
readonly property var filteredProcesses: {
if (!DgopService.allProcesses || DgopService.allProcesses.length === 0)
return [];
let procs = DgopService.allProcesses.slice();
if (searchText.length > 0) {
const search = searchText.toLowerCase();
procs = procs.filter(p => {
const cmd = (p.command || "").toLowerCase();
const fullCmd = (p.fullCommand || "").toLowerCase();
const pid = p.pid.toString();
return cmd.includes(search) || fullCmd.includes(search) || pid.includes(search);
});
}
const asc = DgopService.sortAscending;
procs.sort((a, b) => {
let valueA, valueB, result;
switch (DgopService.currentSort) {
case "cpu":
valueA = a.cpu || 0;
valueB = b.cpu || 0;
result = valueB - valueA;
break;
case "memory":
valueA = a.memoryKB || 0;
valueB = b.memoryKB || 0;
result = valueB - valueA;
break;
case "name":
valueA = (a.command || "").toLowerCase();
valueB = (b.command || "").toLowerCase();
result = valueA.localeCompare(valueB);
break;
case "pid":
valueA = a.pid || 0;
valueB = b.pid || 0;
result = valueA - valueB;
break;
default:
return 0;
}
return asc ? -result : result;
});
return procs;
}
Component.onCompleted: {
DgopService.addRef(["processes", "cpu", "memory", "system"]);
cachedProcesses = filteredProcesses;
}
Component.onDestruction: {
DgopService.removeRef(["processes", "cpu", "memory", "system"]);
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 36
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: 0
SortableHeader {
Layout.fillWidth: true
Layout.minimumWidth: 200
text: I18n.tr("Name")
sortKey: "name"
currentSort: DgopService.currentSort
sortAscending: DgopService.sortAscending
onClicked: DgopService.toggleSort("name")
alignment: Text.AlignLeft
}
SortableHeader {
Layout.preferredWidth: 100
text: "CPU"
sortKey: "cpu"
currentSort: DgopService.currentSort
sortAscending: DgopService.sortAscending
onClicked: DgopService.toggleSort("cpu")
}
SortableHeader {
Layout.preferredWidth: 100
text: I18n.tr("Memory")
sortKey: "memory"
currentSort: DgopService.currentSort
sortAscending: DgopService.sortAscending
onClicked: DgopService.toggleSort("memory")
}
SortableHeader {
Layout.preferredWidth: 80
text: "PID"
sortKey: "pid"
currentSort: DgopService.currentSort
sortAscending: DgopService.sortAscending
onClicked: DgopService.toggleSort("pid")
}
Item {
Layout.preferredWidth: 40
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Theme.outlineLight
}
DankListView {
id: processListView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 2
model: ScriptModel {
values: root.cachedProcesses
objectProp: "pid"
}
delegate: ProcessItem {
required property var modelData
width: processListView.width
process: modelData
isExpanded: root.expandedPid === (modelData?.pid ?? -1).toString()
contextMenu: root.contextMenu
onToggleExpand: {
const pidStr = (modelData?.pid ?? -1).toString();
root.expandedPid = (root.expandedPid === pidStr) ? "" : pidStr;
}
onHoveringExpandedChanged: {
if (hoveringExpanded)
root.hoveringExpandedItem = true;
else
Qt.callLater(() => {
root.hoveringExpandedItem = false;
});
}
}
Rectangle {
anchors.centerIn: parent
width: 300
height: 100
radius: Theme.cornerRadius
color: "transparent"
visible: root.cachedProcesses.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: root.searchText.length > 0 ? "search_off" : "hourglass_empty"
size: 32
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No matching processes", "empty state in process list")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
visible: root.searchText.length > 0
}
}
}
}
}
component SortableHeader: Item {
id: headerItem
property string text: ""
property string sortKey: ""
property string currentSort: ""
property bool sortAscending: false
property int alignment: Text.AlignHCenter
signal clicked
readonly property bool isActive: sortKey === currentSort
height: 36
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: Theme.cornerRadius
color: headerItem.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (headerMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06) : "transparent")
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: 4
Item {
Layout.fillWidth: headerItem.alignment === Text.AlignLeft
visible: headerItem.alignment !== Text.AlignLeft
}
StyledText {
text: headerItem.text
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: headerItem.isActive ? Font.Bold : Font.Medium
color: headerItem.isActive ? Theme.primary : Theme.surfaceText
opacity: headerItem.isActive ? 1 : 0.8
}
DankIcon {
name: headerItem.sortAscending ? "arrow_upward" : "arrow_downward"
size: Theme.fontSizeSmall
color: Theme.primary
visible: headerItem.isActive
}
Item {
Layout.fillWidth: headerItem.alignment !== Text.AlignLeft
visible: headerItem.alignment === Text.AlignLeft
}
}
MouseArea {
id: headerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: headerItem.clicked()
}
}
component ProcessItem: Rectangle {
id: processItemRoot
property var process: null
property bool isExpanded: false
property var contextMenu: null
readonly property bool hoveringExpanded: (isExpanded && processMouseArea.containsMouse) || copyMouseArea.containsMouse
signal toggleExpand
readonly property int processPid: process?.pid ?? 0
readonly property real processCpu: process?.cpu ?? 0
readonly property int processMemKB: process?.memoryKB ?? 0
readonly property string processCmd: process?.command ?? ""
readonly property string processFullCmd: process?.fullCommand ?? processCmd
height: isExpanded ? (44 + expandedRect.height + Theme.spacingXS) : 44
radius: Theme.cornerRadius
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.width: 1
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
MouseArea {
id: processMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (processItemRoot.processPid > 0 && processItemRoot.contextMenu) {
processItemRoot.contextMenu.processData = processItemRoot.process;
const globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y);
const localPos = processItemRoot.contextMenu.parent ? processItemRoot.contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
processItemRoot.contextMenu.show(localPos.x, localPos.y);
}
return;
}
processItemRoot.toggleExpand();
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 44
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: 0
Item {
Layout.fillWidth: true
Layout.minimumWidth: 200
height: parent.height
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: DgopService.getProcessIcon(processItemRoot.processCmd)
size: Theme.iconSize - 4
color: {
if (processItemRoot.processCpu > 80)
return Theme.error;
if (processItemRoot.processCpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
opacity: 0.8
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: processItemRoot.processCmd
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: Math.min(implicitWidth, 280)
anchors.verticalCenter: parent.verticalCenter
}
}
}
Item {
Layout.preferredWidth: 100
height: parent.height
Rectangle {
anchors.centerIn: parent
width: 70
height: 24
radius: Theme.cornerRadius
color: {
if (processItemRoot.processCpu > 80)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
if (processItemRoot.processCpu > 50)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
}
StyledText {
anchors.centerIn: parent
text: DgopService.formatCpuUsage(processItemRoot.processCpu)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (processItemRoot.processCpu > 80)
return Theme.error;
if (processItemRoot.processCpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
}
}
}
Item {
Layout.preferredWidth: 100
height: parent.height
Rectangle {
anchors.centerIn: parent
width: 70
height: 24
radius: Theme.cornerRadius
color: {
if (processItemRoot.processMemKB > 2 * 1024 * 1024)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
if (processItemRoot.processMemKB > 1024 * 1024)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
}
StyledText {
anchors.centerIn: parent
text: DgopService.formatMemoryUsage(processItemRoot.processMemKB)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (processItemRoot.processMemKB > 2 * 1024 * 1024)
return Theme.error;
if (processItemRoot.processMemKB > 1024 * 1024)
return Theme.warning;
return Theme.surfaceText;
}
}
}
}
Item {
Layout.preferredWidth: 80
height: parent.height
StyledText {
anchors.centerIn: parent
text: processItemRoot.processPid > 0 ? processItemRoot.processPid.toString() : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
}
}
Item {
Layout.preferredWidth: 40
height: parent.height
DankIcon {
anchors.centerIn: parent
name: processItemRoot.isExpanded ? "expand_less" : "expand_more"
size: Theme.iconSize - 4
color: Theme.surfaceVariantText
}
}
}
}
Rectangle {
id: expandedRect
width: parent.width - Theme.spacingM * 2
height: processItemRoot.isExpanded ? (expandedContent.implicitHeight + Theme.spacingS * 2) : 0
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius - 2
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.6)
clip: true
visible: processItemRoot.isExpanded
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Column {
id: expandedContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingS
spacing: Theme.spacingXS
RowLayout {
width: parent.width
spacing: Theme.spacingS
StyledText {
id: cmdLabel
text: I18n.tr("Full Command:", "process detail label")
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter
}
StyledText {
id: cmdText
text: processItemRoot.processFullCmd
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
elide: Text.ElideMiddle
}
Rectangle {
id: copyBtn
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
radius: Theme.cornerRadius - 2
color: copyMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "content_copy"
size: 14
color: copyMouseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: copyMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["dms", "cl", "copy", processItemRoot.processFullCmd]);
}
}
}
}
Row {
spacing: Theme.spacingL
Row {
spacing: Theme.spacingXS
StyledText {
text: "PPID:"
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
}
StyledText {
text: (processItemRoot.process?.ppid ?? 0) > 0 ? processItemRoot.process.ppid.toString() : "--"
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
}
Row {
spacing: Theme.spacingXS
StyledText {
text: "Mem:"
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
}
StyledText {
text: (processItemRoot.process?.memoryPercent ?? 0).toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
}
}
}
}
}
}
}

View File

@@ -1,435 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Row {
width: parent.width
spacing: Theme.spacingM
Component.onCompleted: {
DgopService.addRef(["cpu", "memory", "system"]);
}
Component.onDestruction: {
DgopService.removeRef(["cpu", "memory", "system"]);
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadius
color: {
if (DgopService.sortBy === "cpu") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
} else if (cpuCardMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
}
}
border.color: DgopService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: DgopService.sortBy === "cpu" ? 2 : 1
MouseArea {
id: cpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("cpu");
}
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("CPU")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: DgopService.sortBy === "cpu" ? Theme.primary : Theme.secondary
opacity: DgopService.sortBy === "cpu" ? 1 : 0.8
}
Row {
spacing: Theme.spacingS
StyledText {
text: {
if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null) {
return "--%";
}
return DgopService.cpuUsage.toFixed(1) + "%";
}
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 1
height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature <= 0) {
return "--°";
}
return Math.round(DgopService.cpuTemperature) + "°";
}
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: {
if (DgopService.cpuTemperature > 80) {
return Theme.error;
}
if (DgopService.cpuTemperature > 60) {
return Theme.warning;
}
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: `${DgopService.cpuCores} cores`
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadius
color: {
if (DgopService.sortBy === "memory") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
} else if (memoryCardMouseArea.containsMouse) {
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12);
} else {
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08);
}
}
border.color: DgopService.sortBy === "memory" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.2)
border.width: DgopService.sortBy === "memory" ? 2 : 1
MouseArea {
id: memoryCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("memory");
}
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("Memory")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: DgopService.sortBy === "memory" ? Theme.primary : Theme.secondary
opacity: DgopService.sortBy === "memory" ? 1 : 0.8
}
Row {
spacing: Theme.spacingS
StyledText {
text: DgopService.formatSystemMemory(DgopService.usedMemoryKB)
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 1
height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
visible: DgopService.totalSwapKB > 0
}
StyledText {
text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(DgopService.usedSwapKB) : ""
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: DgopService.totalSwapKB > 0
}
}
StyledText {
text: {
if (DgopService.totalSwapKB > 0) {
return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB) + " + swap";
}
return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB);
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadius
color: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16);
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
const vendor = gpu.vendor.toLowerCase();
if (vendor.includes("nvidia")) {
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2);
} else {
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12);
}
} else if (vendor.includes("amd")) {
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
} else {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
}
} else if (vendor.includes("intel")) {
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2);
} else {
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.12);
}
}
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16);
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
}
border.color: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
const vendor = gpu.vendor.toLowerCase();
if (vendor.includes("nvidia")) {
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3);
} else if (vendor.includes("amd")) {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
} else if (vendor.includes("intel")) {
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3);
}
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
}
border.width: 1
MouseArea {
id: gpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
if (DgopService.availableGpus.length > 1) {
const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length;
SessionData.setSelectedGpuIndex(nextIndex);
}
} else if (mouse.button === Qt.RightButton) {
gpuContextMenu.popup();
}
}
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("GPU")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.secondary
opacity: 0.8
}
StyledText {
text: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return "No GPU";
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
// Check if temperature monitoring is enabled for this GPU
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
const temp = gpu.temperature;
const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0;
if (hasTemp) {
return Math.round(temp) + "°";
} else {
return gpu.vendor;
}
}
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return Theme.surfaceText;
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
const temp = gpu.temperature || 0;
if (tempEnabled && temp > 80) {
return Theme.error;
}
if (tempEnabled && temp > 60) {
return Theme.warning;
}
return Theme.surfaceText;
}
}
StyledText {
text: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return "No GPUs detected";
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
const temp = gpu.temperature;
const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0;
if (hasTemp) {
return gpu.vendor + " " + gpu.displayName;
} else {
return gpu.displayName;
}
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
width: parent.parent.width - Theme.spacingM * 2
elide: Text.ElideRight
maximumLineCount: 1
}
}
Menu {
id: gpuContextMenu
MenuItem {
text: I18n.tr("Enable GPU Temperature")
checkable: true
checked: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return false;
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
if (!gpu.pciId) {
return false;
}
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 : false;
}
onTriggered: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return;
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
if (!gpu.pciId) {
return;
}
const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : [];
const index = enabledIds.indexOf(gpu.pciId);
if (checked && index === -1) {
enabledIds.push(gpu.pciId);
DgopService.addGpuPciId(gpu.pciId);
} else if (!checked && index !== -1) {
enabledIds.splice(index, 1);
DgopService.removeGpuPciId(gpu.pciId);
}
SessionData.setEnabledGpuPciIds(enabledIds);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}

View File

@@ -1,591 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankFlickable {
anchors.fill: parent
contentHeight: systemColumn.implicitHeight
clip: true
Component.onCompleted: {
DgopService.addRef(["system", "diskmounts"]);
}
Component.onDestruction: {
DgopService.removeRef(["system", "diskmounts"]);
}
Column {
id: systemColumn
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: parent.width
height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
id: systemInfoColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingL
SystemLogo {
width: 80
height: 80
}
Column {
width: parent.width - 80 - Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: DgopService.hostname
font.pixelSize: Theme.fontSizeXLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Light
color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: `${DgopService.distribution} ${DgopService.architecture} ${DgopService.kernelVersion}`
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: `${DgopService.uptime} Boot: ${DgopService.bootTime}`
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: `Load: ${DgopService.loadAverage} ${DgopService.processCount} processes, ${DgopService.threadCount} threads`
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Row {
width: parent.width
spacing: Theme.spacingXL
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: hardwareColumn.implicitHeight + Theme.spacingL
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
Column {
id: hardwareColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("System")
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: DgopService.cpuModel
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: DgopService.motherboard
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: `BIOS ${DgopService.biosVersion}`
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: `${DgopService.formatSystemMemory(DgopService.totalMemoryKB)} RAM`
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: gpuColumn.implicitHeight + Theme.spacingL
radius: Theme.cornerRadius
color: {
const baseColor = Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
const hoverColor = Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.popupTransparency * 1.5);
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor;
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
const vendor = gpu.fullName.split(' ')[0].toLowerCase();
let tintColor;
if (vendor.includes("nvidia")) {
tintColor = Theme.success;
} else if (vendor.includes("amd")) {
tintColor = Theme.error;
} else if (vendor.includes("intel")) {
tintColor = Theme.info;
} else {
return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor;
}
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
return Qt.rgba((hoverColor.r + tintColor.r * 0.1) / 1.1, (hoverColor.g + tintColor.g * 0.1) / 1.1, (hoverColor.b + tintColor.b * 0.1) / 1.1, 0.6);
} else {
return Qt.rgba((baseColor.r + tintColor.r * 0.08) / 1.08, (baseColor.g + tintColor.g * 0.08) / 1.08, (baseColor.b + tintColor.b * 0.08) / 1.08, 0.4);
}
}
border.width: 1
border.color: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1);
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
const vendor = gpu.fullName.split(' ')[0].toLowerCase();
if (vendor.includes("nvidia")) {
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3);
} else if (vendor.includes("amd")) {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
} else if (vendor.includes("intel")) {
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3);
}
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1);
}
MouseArea {
id: gpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (DgopService.availableGpus.length > 1) {
const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length;
SessionData.setSelectedGpuIndex(nextIndex);
}
}
}
Column {
id: gpuColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.iconSizeSmall
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "GPU"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return "No GPUs detected";
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
return gpu.fullName;
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return "Device: N/A";
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
return `Device: ${gpu.pciId}`;
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
textFormat: Text.RichText
}
StyledText {
text: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return "Driver: N/A";
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
return `Driver: ${gpu.driver}`;
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return "Temp: --°";
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
const temp = gpu.temperature;
return `Temp: ${(temp === undefined || temp === null || temp === 0) ? '--°' : `${Math.round(temp)}°C`}`;
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
}
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
const temp = gpu.temperature || 0;
if (temp > 80) {
return Theme.error;
}
if (temp > 60) {
return Theme.warning;
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
}
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
width: parent.width
height: storageColumn.implicitHeight + 2 * Theme.spacingL
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
id: storageColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "storage"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Storage & Disks")
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: 2
Row {
width: parent.width
height: 24
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Device")
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: I18n.tr("Mount")
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: I18n.tr("Used")
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: I18n.tr("Available")
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: I18n.tr("Use%")
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.1
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
Repeater {
id: diskMountRepeater
model: DgopService.diskMounts
Rectangle {
width: parent.width
height: 24
radius: Theme.cornerRadius
color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent"
MouseArea {
id: diskMouseArea
anchors.fill: parent
hoverEnabled: true
}
Row {
anchors.fill: parent
spacing: Theme.spacingS
StyledText {
text: modelData.device
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.size
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.used
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.avail
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.percent
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: {
const percent = parseInt(modelData.percent);
if (percent > 90) {
return Theme.error;
}
if (percent > 75) {
return Theme.warning;
}
return Theme.surfaceText;
}
width: parent.width * 0.1
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,386 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
Component.onCompleted: {
DgopService.addRef(["system", "cpu"]);
}
Component.onDestruction: {
DgopService.removeRef(["system", "cpu"]);
}
ColumnLayout {
anchors.fill: parent
spacing: Theme.spacingM
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: systemInfoColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
ColumnLayout {
id: systemInfoColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Row {
spacing: Theme.spacingS
DankIcon {
name: "computer"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("System Information", "system info header in system monitor")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
GridLayout {
Layout.fillWidth: true
columns: 2
rowSpacing: Theme.spacingS
columnSpacing: Theme.spacingXL
InfoRow {
label: I18n.tr("Hostname", "system info label")
value: DgopService.hostname || "--"
}
InfoRow {
label: I18n.tr("Distribution", "system info label")
value: DgopService.distribution || "--"
}
InfoRow {
label: I18n.tr("Kernel", "system info label")
value: DgopService.kernelVersion || "--"
}
InfoRow {
label: I18n.tr("Architecture", "system info label")
value: DgopService.architecture || "--"
}
InfoRow {
label: I18n.tr("CPU")
value: DgopService.cpuModel || ("" + DgopService.cpuCores + " cores")
}
InfoRow {
label: I18n.tr("Uptime")
value: DgopService.uptime || "--"
}
InfoRow {
label: I18n.tr("Load Average", "system info label")
value: DgopService.loadAverage || "--"
}
InfoRow {
label: I18n.tr("Processes")
value: DgopService.processCount > 0 ? DgopService.processCount.toString() : "--"
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Row {
spacing: Theme.spacingS
DankIcon {
name: "developer_board"
size: Theme.iconSize
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("GPU Monitoring", "gpu section header in system monitor")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Theme.outlineLight
}
DankListView {
id: gpuListView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 8
model: DgopService.availableGpus
delegate: Rectangle {
required property var modelData
required property int index
width: gpuListView.width
height: 80
radius: Theme.cornerRadius
color: {
const vendor = (modelData?.vendor ?? "").toLowerCase();
if (vendor.includes("nvidia"))
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.08);
if (vendor.includes("amd"))
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
if (vendor.includes("intel"))
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: {
const vendor = (modelData?.vendor ?? "").toLowerCase();
if (vendor.includes("nvidia"))
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2);
if (vendor.includes("amd"))
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
if (vendor.includes("intel"))
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
}
border.width: 1
readonly property bool tempEnabled: {
const pciId = modelData?.pciId ?? "";
if (!pciId)
return false;
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(pciId) !== -1 : false;
}
RowLayout {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "developer_board"
size: Theme.iconSize + 4
color: {
const vendor = (modelData?.vendor ?? "").toLowerCase();
if (vendor.includes("nvidia"))
return Theme.success;
if (vendor.includes("amd"))
return Theme.error;
if (vendor.includes("intel"))
return Theme.info;
return Theme.surfaceVariantText;
}
}
Column {
Layout.fillWidth: true
spacing: Theme.spacingXS
StyledText {
text: modelData?.displayName ?? I18n.tr("Unknown GPU", "fallback gpu name")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
}
Row {
spacing: Theme.spacingS
StyledText {
text: modelData?.vendor ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: (modelData?.driver ?? "").length > 0
}
StyledText {
text: modelData?.driver ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: (modelData?.driver ?? "").length > 0
}
}
StyledText {
text: modelData?.pciId ?? ""
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
opacity: 0.7
}
}
Rectangle {
width: 70
height: 32
radius: Theme.cornerRadius
color: parent.parent.tempEnabled ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
border.color: tempMouseArea.containsMouse ? Theme.outline : "transparent"
border.width: 1
Row {
id: tempRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "thermostat"
size: 16
color: {
if (!parent.parent.parent.parent.tempEnabled)
return Theme.surfaceVariantText;
const temp = modelData?.temperature ?? 0;
if (temp > 85)
return Theme.error;
if (temp > 70)
return Theme.warning;
return Theme.surfaceText;
}
opacity: parent.parent.parent.parent.tempEnabled ? 1 : 0.5
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!parent.parent.parent.parent.tempEnabled)
return I18n.tr("Off");
const temp = modelData?.temperature ?? 0;
return temp > 0 ? (temp.toFixed(0) + "°C") : "--";
}
font.pixelSize: Theme.fontSizeSmall
font.family: parent.parent.parent.parent.tempEnabled ? SettingsData.monoFontFamily : ""
font.weight: parent.parent.parent.parent.tempEnabled ? Font.Bold : Font.Normal
color: {
if (!parent.parent.parent.parent.tempEnabled)
return Theme.surfaceVariantText;
const temp = modelData?.temperature ?? 0;
if (temp > 85)
return Theme.error;
if (temp > 70)
return Theme.warning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: tempMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const pciId = modelData?.pciId;
if (!pciId)
return;
const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : [];
const idx = enabledIds.indexOf(pciId);
const wasEnabled = idx !== -1;
if (!wasEnabled) {
enabledIds.push(pciId);
DgopService.addGpuPciId(pciId);
} else {
enabledIds.splice(idx, 1);
DgopService.removeGpuPciId(pciId);
}
SessionData.setEnabledGpuPciIds(enabledIds);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
Rectangle {
anchors.centerIn: parent
width: 300
height: 100
radius: Theme.cornerRadius
color: "transparent"
visible: DgopService.availableGpus.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: "developer_board_off"
size: 32
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No GPUs detected", "empty state in gpu list")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}
component InfoRow: RowLayout {
property string label: ""
property string value: ""
Layout.fillWidth: true
spacing: Theme.spacingS
StyledText {
text: label + ":"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 100
}
StyledText {
text: value
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
Layout.fillWidth: true
elide: Text.ElideRight
}
}
}

View File

@@ -177,10 +177,11 @@ Item {
StyledText {
text: {
if (!SystemUpdateService.shellVersion)
if (!SystemUpdateService.shellVersion && !DMSService.cliVersion)
return "dms";
let version = SystemUpdateService.shellVersion;
let version = SystemUpdateService.shellVersion || "";
let cliVersion = DMSService.cliVersion || "";
// Debian/Ubuntu/OpenSUSE git format: 1.0.3+git2264.c5c5ce84
let match = version.match(/^([\d.]+)\+git(\d+)\./);
@@ -191,7 +192,25 @@ Item {
// Fedora COPR git format: 0.0.git.2267.d430cae9
match = version.match(/^[\d.]+\.git\.(\d+)\./);
if (match) {
return `dms (git) v1.0.3-${match[1]}`;
function extractBaseVersion(value) {
if (!value)
return "";
let baseMatch = value.match(/(\d+\.\d+\.\d+)/);
if (baseMatch)
return baseMatch[1];
baseMatch = value.match(/(\d+\.\d+)/);
if (baseMatch)
return baseMatch[1];
return "";
}
let baseVersion = extractBaseVersion(cliVersion);
if (!baseVersion)
baseVersion = extractBaseVersion(SystemUpdateService.semverVersion);
if (baseVersion) {
return `dms (git) v${baseVersion}-${match[1]}`;
}
return `dms (git) v${match[1]}`;
}
// Stable release format: 1.0.3
@@ -200,6 +219,18 @@ Item {
return `dms v${match[1]}`;
}
if (!version && cliVersion) {
match = cliVersion.match(/^([\d.]+)\+git(\d+)\./);
if (match) {
return `dms (git) v${match[1]}-${match[2]}`;
}
match = cliVersion.match(/^([\d.]+)$/);
if (match) {
return `dms v${match[1]}`;
}
return `dms ${cliVersion}`;
}
return `dms ${version}`;
}
font.pixelSize: Theme.fontSizeXLarge

View File

@@ -125,6 +125,33 @@ Item {
}
]
readonly property var maxPinnedOptions: [
{
text: "5",
value: 5
},
{
text: "10",
value: 10
},
{
text: "15",
value: 15
},
{
text: "25",
value: 25
},
{
text: "50",
value: 50
},
{
text: "100",
value: 100
}
]
function getMaxHistoryText(value) {
if (value <= 0)
return "∞";
@@ -152,6 +179,14 @@ Item {
return value + " " + I18n.tr("days");
}
function getMaxPinnedText(value) {
for (let opt of maxPinnedOptions) {
if (opt.value === value)
return opt.text;
}
return value.toString();
}
function loadConfig() {
configLoaded = false;
configError = false;
@@ -295,6 +330,24 @@ Item {
}
}
}
SettingsDropdownRow {
tab: "clipboard"
tags: ["clipboard", "pinned", "max", "limit"]
settingKey: "maxPinned"
text: I18n.tr("Maximum Pinned Entries")
description: I18n.tr("Maximum number of entries that can be saved")
currentValue: root.getMaxPinnedText(root.config.maxPinned ?? 25)
options: root.maxPinnedOptions.map(opt => opt.text)
onValueChanged: value => {
for (let opt of root.maxPinnedOptions) {
if (opt.text === value) {
root.saveConfig("maxPinned", opt.value);
return;
}
}
}
}
}
SettingsCard {

View File

@@ -22,18 +22,38 @@ Item {
readonly property var allInstances: SettingsData.desktopWidgetInstances || []
readonly property var allGroups: SettingsData.desktopWidgetGroups || []
DesktopWidgetBrowser {
id: widgetBrowser
parentModal: root.parentModal
onWidgetAdded: widgetType => {
ToastService.showInfo(I18n.tr("Widget added"));
function showWidgetBrowser() {
widgetBrowserLoader.active = true;
if (widgetBrowserLoader.item)
widgetBrowserLoader.item.show();
}
function showDesktopPluginBrowser() {
desktopPluginBrowserLoader.active = true;
if (desktopPluginBrowserLoader.item)
desktopPluginBrowserLoader.item.show();
}
LazyLoader {
id: widgetBrowserLoader
active: false
DesktopWidgetBrowser {
parentModal: root.parentModal
onWidgetAdded: widgetType => {
ToastService.showInfo(I18n.tr("Widget added"));
}
}
}
PluginBrowser {
id: desktopPluginBrowser
parentModal: root.parentModal
typeFilter: "desktop-widget"
LazyLoader {
id: desktopPluginBrowserLoader
active: false
PluginBrowser {
parentModal: root.parentModal
typeFilter: "desktop-widget"
}
}
DankFlickable {
@@ -74,13 +94,13 @@ Item {
DankButton {
text: I18n.tr("Add Widget")
iconName: "add"
onClicked: widgetBrowser.show()
onClicked: root.showWidgetBrowser()
}
DankButton {
text: I18n.tr("Browse Plugins")
iconName: "store"
onClicked: desktopPluginBrowser.show()
onClicked: root.showDesktopPluginBrowser()
}
}
}

View File

@@ -29,6 +29,9 @@ Item {
SettingsButtonGroupRow {
text: I18n.tr("Position")
model: ["Top", "Bottom", "Left", "Right"]
buttonPadding: Theme.spacingS
minButtonWidth: 44
textSize: Theme.fontSizeSmall
currentIndex: {
switch (SettingsData.dockPosition) {
case SettingsData.Position.Top:
@@ -129,6 +132,9 @@ Item {
tags: ["dock", "indicator", "style", "circle", "line"]
text: I18n.tr("Indicator Style")
model: ["Circle", "Line"]
buttonPadding: Theme.spacingS
minButtonWidth: 44
textSize: Theme.fontSizeSmall
currentIndex: SettingsData.dockIndicatorStyle === "circle" ? 0 : 1
onSelectionChanged: (index, selected) => {
if (selected) {
@@ -225,6 +231,9 @@ Item {
description: I18n.tr("Choose the border accent color")
visible: SettingsData.dockBorderEnabled
model: ["Surface", "Secondary", "Primary"]
buttonPadding: Theme.spacingS
minButtonWidth: 44
textSize: Theme.fontSizeSmall
currentIndex: {
switch (SettingsData.dockBorderColor) {
case "surfaceText":

View File

@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
@@ -14,6 +15,7 @@ Item {
property string expandedVpnUuid: ""
property string expandedWifiSsid: ""
property string expandedEthDevice: ""
property int maxPinnedWifiNetworks: 3
Component.onCompleted: {
NetworkService.addRef();
@@ -23,15 +25,59 @@ Item {
NetworkService.removeRef();
}
FileBrowserModal {
id: vpnFileBrowser
browserTitle: I18n.tr("Import VPN")
browserIcon: "vpn_key"
browserType: "vpn"
fileExtensions: VPNService.getFileFilter()
function openVpnFileBrowser() {
vpnFileBrowserLoader.active = true;
if (vpnFileBrowserLoader.item)
vpnFileBrowserLoader.item.open();
}
onFileSelected: path => {
VPNService.importVpn(path.replace("file://", ""));
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedWifiNetworks() {
const pins = SettingsData.wifiNetworkPins || {}
return normalizePinList(pins["preferredWifi"])
}
function toggleWifiPin(ssid) {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}))
let pinnedList = normalizePinList(pins["preferredWifi"])
const pinIndex = pinnedList.indexOf(ssid)
if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1)
} else {
pinnedList.unshift(ssid)
if (pinnedList.length > maxPinnedWifiNetworks)
pinnedList = pinnedList.slice(0, maxPinnedWifiNetworks)
}
if (pinnedList.length > 0)
pins["preferredWifi"] = pinnedList
else
delete pins["preferredWifi"]
SettingsData.set("wifiNetworkPins", pins)
}
LazyLoader {
id: vpnFileBrowserLoader
active: false
FileBrowserModal {
browserTitle: I18n.tr("Import VPN")
browserIcon: "vpn_key"
browserType: "vpn"
fileExtensions: VPNService.getFileFilter()
onFileSelected: path => {
VPNService.importVpn(path.replace("file://", ""));
}
}
}
@@ -1014,15 +1060,19 @@ Item {
model: {
const ssid = NetworkService.currentWifiSSID;
const networks = NetworkService.wifiNetworks || [];
const pins = SettingsData.wifiNetworkPins || {};
const pinnedSSID = pins["preferredWifi"];
const pinnedList = networkTab.getPinnedWifiNetworks();
let sorted = [...networks];
sorted.sort((a, b) => {
if (a.ssid === pinnedSSID && b.ssid !== pinnedSSID)
return -1;
if (b.ssid === pinnedSSID && a.ssid !== pinnedSSID)
return 1;
const aPinnedIndex = pinnedList.indexOf(a.ssid)
const bPinnedIndex = pinnedList.indexOf(b.ssid)
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1)
return 1
if (bPinnedIndex === -1)
return -1
return aPinnedIndex - bPinnedIndex
}
if (a.ssid === ssid)
return -1;
if (b.ssid === ssid)
@@ -1038,7 +1088,7 @@ Item {
required property int index
readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID
readonly property bool isPinned: (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid
readonly property bool isPinned: networkTab.getPinnedWifiNetworks().includes(modelData.ssid)
readonly property bool isExpanded: networkTab.expandedWifiSsid === modelData.ssid
width: parent.width
@@ -1213,13 +1263,7 @@ Item {
buttonSize: 28
iconColor: isPinned ? Theme.primary : Theme.surfaceVariantText
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
if (isPinned) {
delete pins["preferredWifi"];
} else {
pins["preferredWifi"] = modelData.ssid;
}
SettingsData.set("wifiNetworkPins", pins);
networkTab.toggleWifiPin(modelData.ssid)
}
}
@@ -1520,7 +1564,7 @@ Item {
hoverEnabled: true
cursorShape: VPNService.importing ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !VPNService.importing
onClicked: vpnFileBrowser.open()
onClicked: networkTab.openVpnFileBrowser()
}
}

View File

@@ -270,7 +270,9 @@ FloatingWindow {
root.updateFilteredPlugins();
return;
}
thirdPartyConfirmModal.visible = true;
thirdPartyConfirmLoader.active = true;
if (thirdPartyConfirmLoader.item)
thirdPartyConfirmLoader.item.show();
}
}
@@ -668,119 +670,132 @@ FloatingWindow {
}
}
FloatingWindow {
id: thirdPartyConfirmModal
LazyLoader {
id: thirdPartyConfirmLoader
active: false
objectName: "thirdPartyConfirm"
title: I18n.tr("Third-Party Plugin Warning")
implicitWidth: 500
implicitHeight: 350
color: Theme.surfaceContainer
visible: false
FloatingWindow {
id: thirdPartyConfirmModal
FocusScope {
anchors.fill: parent
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
thirdPartyConfirmModal.visible = false;
event.accepted = true;
}
function show() {
visible = true;
}
Column {
function hide() {
visible = false;
}
objectName: "thirdPartyConfirm"
title: I18n.tr("Third-Party Plugin Warning")
implicitWidth: 500
implicitHeight: 350
color: Theme.surfaceContainer
visible: false
FocusScope {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
focus: true
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "warning"
size: Theme.iconSize
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
thirdPartyConfirmModal.hide();
event.accepted = true;
}
StyledText {
text: I18n.tr("Third-Party Plugin Warning")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - parent.spacing * 2 - Theme.iconSize - parent.children[1].implicitWidth - closeConfirmBtn.width
height: 1
}
DankActionButton {
id: closeConfirmBtn
iconName: "close"
iconSize: Theme.iconSize - 2
iconColor: Theme.outline
anchors.verticalCenter: parent.verticalCenter
onClicked: thirdPartyConfirmModal.visible = false
}
}
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
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
StyledText {
text: I18n.tr("• Plugins may contain bugs or security issues")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
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
}
Item {
width: parent.width - parent.spacing * 2 - Theme.iconSize - parent.children[1].implicitWidth - closeConfirmBtn.width
height: 1
}
DankActionButton {
id: closeConfirmBtn
iconName: "close"
iconSize: Theme.iconSize - 2
iconColor: Theme.outline
anchors.verticalCenter: parent.verticalCenter
onClicked: thirdPartyConfirmModal.hide()
}
}
StyledText {
text: I18n.tr("• Review code before installation when possible")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
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
}
StyledText {
text: I18n.tr("• Install only from trusted sources")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: parent.height - parent.spacing * 3 - y
}
StyledText {
text: I18n.tr("• Plugins may contain bugs or security issues")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Row {
anchors.right: parent.right
spacing: Theme.spacingM
StyledText {
text: I18n.tr("• Review code before installation when possible")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankButton {
text: I18n.tr("Cancel")
iconName: "close"
onClicked: thirdPartyConfirmModal.visible = false
StyledText {
text: I18n.tr("• Install only from trusted sources")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
DankButton {
text: I18n.tr("I Understand")
iconName: "check"
onClicked: {
SessionData.setShowThirdPartyPlugins(true);
root.updateFilteredPlugins();
thirdPartyConfirmModal.visible = false;
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.hide()
}
DankButton {
text: I18n.tr("I Understand")
iconName: "check"
onClicked: {
SessionData.setShowThirdPartyPlugins(true);
root.updateFilteredPlugins();
thirdPartyConfirmModal.hide();
}
}
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
@@ -209,7 +210,7 @@ FocusScope {
iconName: "store"
enabled: DMSService.dmsAvailable
onClicked: {
pluginBrowser.show();
showPluginBrowser();
}
}
@@ -382,9 +383,11 @@ FocusScope {
Connections {
target: DMSService
function onPluginsListReceived(plugins) {
pluginBrowser.isLoading = false;
pluginBrowser.allPlugins = plugins;
pluginBrowser.updateFilteredPlugins();
if (!pluginBrowserLoader.item)
return;
pluginBrowserLoader.item.isLoading = false;
pluginBrowserLoader.item.allPlugins = plugins;
pluginBrowserLoader.item.updateFilteredPlugins();
}
function onInstalledPluginsReceived(plugins) {
var pluginMap = {};
@@ -410,22 +413,36 @@ FocusScope {
}
Component.onCompleted: {
pluginBrowser.parentModal = pluginsTab.parentModal;
if (DMSService.dmsAvailable && DMSService.apiVersion >= 8)
DMSService.listInstalled();
if (PopoutService.pendingPluginInstall)
Qt.callLater(() => pluginBrowser.show());
Qt.callLater(showPluginBrowser);
}
Connections {
target: PopoutService
function onPendingPluginInstallChanged() {
if (PopoutService.pendingPluginInstall)
pluginBrowser.show();
showPluginBrowser();
}
}
PluginBrowser {
id: pluginBrowser
LazyLoader {
id: pluginBrowserLoader
active: false
PluginBrowser {
id: pluginBrowserItem
Component.onCompleted: {
pluginBrowserItem.parentModal = pluginsTab.parentModal;
}
}
}
function showPluginBrowser() {
pluginBrowserLoader.active = true;
if (pluginBrowserLoader.item)
pluginBrowserLoader.item.show();
}
}

View File

@@ -131,7 +131,7 @@ Item {
if (DMSService.dmsAvailable)
DMSService.listInstalledThemes();
if (PopoutService.pendingThemeInstall)
Qt.callLater(() => themeBrowser.show());
Qt.callLater(() => showThemeBrowser());
templateCheckProcess.running = true;
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl)
checkCursorIncludeStatus();
@@ -169,7 +169,7 @@ Item {
target: PopoutService
function onPendingThemeInstallChanged() {
if (PopoutService.pendingThemeInstall)
themeBrowser.show();
showThemeBrowser();
}
}
@@ -939,7 +939,7 @@ Item {
text: I18n.tr("Browse Themes", "browse themes button")
iconName: "store"
anchors.horizontalCenter: parent.horizontalCenter
onClicked: themeBrowser.show()
onClicked: showThemeBrowser()
}
}
}
@@ -2041,7 +2041,18 @@ Item {
}
}
ThemeBrowser {
id: themeBrowser
LazyLoader {
id: themeBrowserLoader
active: false
ThemeBrowser {
id: themeBrowserItem
}
}
function showThemeBrowser() {
themeBrowserLoader.active = true;
if (themeBrowserLoader.item)
themeBrowserLoader.item.show();
}
}

View File

@@ -139,7 +139,7 @@ Item {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: mainWallpaperBrowser.open()
onClicked: root.openMainWallpaperBrowser()
}
}
@@ -476,7 +476,7 @@ Item {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: lightWallpaperBrowser.open()
onClicked: root.openLightWallpaperBrowser()
}
}
@@ -660,7 +660,7 @@ Item {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: darkWallpaperBrowser.open()
onClicked: root.openDarkWallpaperBrowser()
}
}
@@ -1242,53 +1242,83 @@ Item {
}
}
FileBrowserModal {
id: mainWallpaperBrowser
parentModal: root.parentModal
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
if (SessionData.perMonitorWallpaper) {
SessionData.setMonitorWallpaper(selectedMonitorName, path);
} else {
SessionData.setWallpaper(path);
function openMainWallpaperBrowser() {
mainWallpaperBrowserLoader.active = true;
if (mainWallpaperBrowserLoader.item)
mainWallpaperBrowserLoader.item.open();
}
function openLightWallpaperBrowser() {
lightWallpaperBrowserLoader.active = true;
if (lightWallpaperBrowserLoader.item)
lightWallpaperBrowserLoader.item.open();
}
function openDarkWallpaperBrowser() {
darkWallpaperBrowserLoader.active = true;
if (darkWallpaperBrowserLoader.item)
darkWallpaperBrowserLoader.item.open();
}
LazyLoader {
id: mainWallpaperBrowserLoader
active: false
FileBrowserModal {
parentModal: root.parentModal
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
if (SessionData.perMonitorWallpaper) {
SessionData.setMonitorWallpaper(selectedMonitorName, path);
} else {
SessionData.setWallpaper(path);
}
close();
}
close();
}
}
FileBrowserModal {
id: lightWallpaperBrowser
parentModal: root.parentModal
browserTitle: I18n.tr("Select Wallpaper", "light mode wallpaper file browser title")
browserIcon: "light_mode"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
SessionData.wallpaperPathLight = path;
SessionData.syncWallpaperForCurrentMode();
SessionData.saveSettings();
close();
LazyLoader {
id: lightWallpaperBrowserLoader
active: false
FileBrowserModal {
parentModal: root.parentModal
browserTitle: I18n.tr("Select Wallpaper", "light mode wallpaper file browser title")
browserIcon: "light_mode"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
SessionData.wallpaperPathLight = path;
SessionData.syncWallpaperForCurrentMode();
SessionData.saveSettings();
close();
}
}
}
FileBrowserModal {
id: darkWallpaperBrowser
parentModal: root.parentModal
browserTitle: I18n.tr("Select Wallpaper", "dark mode wallpaper file browser title")
browserIcon: "dark_mode"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
SessionData.wallpaperPathDark = path;
SessionData.syncWallpaperForCurrentMode();
SessionData.saveSettings();
close();
LazyLoader {
id: darkWallpaperBrowserLoader
active: false
FileBrowserModal {
parentModal: root.parentModal
browserTitle: I18n.tr("Select Wallpaper", "dark mode wallpaper file browser title")
browserIcon: "dark_mode"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
SessionData.wallpaperPathDark = path;
SessionData.syncWallpaperForCurrentMode();
SessionData.saveSettings();
close();
}
}
}
}

View File

@@ -51,6 +51,7 @@ StyledRect {
Row {
spacing: Theme.spacingM
width: parent.width
DankIcon {
id: headerIcon
@@ -69,6 +70,8 @@ StyledRect {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.title !== ""
width: parent.width - (headerIcon.visible ? headerIcon.width + parent.spacing : 0)
horizontalAlignment: Text.AlignLeft
}
}

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
@@ -382,6 +383,7 @@ Item {
widgetObj.showMicPercent = SettingsData.controlCenterShowMicPercent;
widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon;
widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon;
widgetObj.showScreenSharingIcon = SettingsData.controlCenterShowScreenSharingIcon;
}
if (widgetId === "diskUsage")
widgetObj.mountPath = "/";
@@ -400,6 +402,24 @@ Item {
setWidgetsForSection(sectionId, widgets);
}
function cloneWidgetData(widget) {
if (typeof widget === "string")
return {
"id": widget,
"enabled": true
};
var result = {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "minimumWidth", "showSwap", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon"];
for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
}
return result;
}
function handleItemEnabledChanged(sectionId, itemId, enabled) {
var widgets = getWidgetsForSection(sectionId).slice();
for (var i = 0; i < widgets.length; i++) {
@@ -407,42 +427,8 @@ Item {
var widgetId = typeof widget === "string" ? widget : widget.id;
if (widgetId !== itemId)
continue;
if (typeof widget === "string") {
widgets[i] = {
"id": widget,
"enabled": enabled
};
break;
}
var newWidget = {
"id": widget.id,
"enabled": enabled
};
if (widget.size !== undefined)
newWidget.size = widget.size;
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex;
else if (widget.id === "gpuTemp")
newWidget.selectedGpuIndex = 0;
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
else if (widget.id === "gpuTemp")
newWidget.pciId = "";
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showAudioPercent = widget.showAudioPercent ?? SettingsData.controlCenterShowAudioPercent;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showBrightnessPercent = widget.showBrightnessPercent ?? SettingsData.controlCenterShowBrightnessPercent;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showMicPercent = widget.showMicPercent ?? SettingsData.controlCenterShowMicPercent;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
var newWidget = cloneWidgetData(widget);
newWidget.enabled = enabled;
widgets[i] = newWidget;
break;
}
@@ -455,128 +441,36 @@ Item {
function handleSpacerSizeChanged(sectionId, widgetIndex, newSize) {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length) {
setWidgetsForSection(sectionId, widgets);
if (widgetIndex < 0 || widgetIndex >= widgets.length)
return;
}
var widget = widgets[widgetIndex];
var widgetId = typeof widget === "string" ? widget : widget.id;
if (widgetId !== "spacer") {
setWidgetsForSection(sectionId, widgets);
if (widgetId !== "spacer")
return;
}
if (typeof widget === "string") {
widgets[widgetIndex] = {
"id": widget,
"enabled": true,
"size": newSize
};
setWidgetsForSection(sectionId, widgets);
return;
}
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"size": newSize
};
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex;
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showAudioPercent = widget.showAudioPercent ?? SettingsData.controlCenterShowAudioPercent;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showBrightnessPercent = widget.showBrightnessPercent ?? SettingsData.controlCenterShowBrightnessPercent;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showMicPercent = widget.showMicPercent ?? SettingsData.controlCenterShowMicPercent;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
var newWidget = cloneWidgetData(widget);
newWidget.size = newSize;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handleGpuSelectionChanged(sectionId, widgetIndex, selectedGpuIndex) {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length) {
setWidgetsForSection(sectionId, widgets);
if (widgetIndex < 0 || widgetIndex >= widgets.length)
return;
}
var pciId = DgopService.availableGpus && DgopService.availableGpus.length > selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : "";
var widget = widgets[widgetIndex];
if (typeof widget === "string") {
widgets[widgetIndex] = {
"id": widget,
"enabled": true,
"selectedGpuIndex": selectedGpuIndex,
"pciId": pciId
};
setWidgetsForSection(sectionId, widgets);
return;
}
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"selectedGpuIndex": selectedGpuIndex,
"pciId": pciId
};
if (widget.size !== undefined)
newWidget.size = widget.size;
var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget.selectedGpuIndex = selectedGpuIndex;
newWidget.pciId = pciId;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handleDiskMountSelectionChanged(sectionId, widgetIndex, mountPath) {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length) {
setWidgetsForSection(sectionId, widgets);
if (widgetIndex < 0 || widgetIndex >= widgets.length)
return;
}
var widget = widgets[widgetIndex];
if (typeof widget === "string") {
widgets[widgetIndex] = {
"id": widget,
"enabled": true,
"mountPath": mountPath
};
setWidgetsForSection(sectionId, widgets);
return;
}
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"mountPath": mountPath
};
if (widget.size !== undefined)
newWidget.size = widget.size;
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex;
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showAudioPercent = widget.showAudioPercent ?? SettingsData.controlCenterShowAudioPercent;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showBrightnessPercent = widget.showBrightnessPercent ?? SettingsData.controlCenterShowBrightnessPercent;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showMicPercent = widget.showMicPercent ?? SettingsData.controlCenterShowMicPercent;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget.mountPath = mountPath;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
@@ -585,32 +479,8 @@ Item {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length)
return;
var widget = widgets[widgetIndex];
if (typeof widget === "string") {
widget = {
"id": widget,
"enabled": true
};
}
var newWidget = {
"id": widget.id,
"enabled": widget.enabled !== undefined ? widget.enabled : true,
"showNetworkIcon": widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon,
"showBluetoothIcon": widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon,
"showAudioIcon": widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon,
"showAudioPercent": widget.showAudioPercent ?? SettingsData.controlCenterShowAudioPercent,
"showVpnIcon": widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon,
"showBrightnessIcon": widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon,
"showBrightnessPercent": widget.showBrightnessPercent ?? SettingsData.controlCenterShowBrightnessPercent,
"showMicIcon": widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon,
"showMicPercent": widget.showMicPercent ?? SettingsData.controlCenterShowMicPercent,
"showBatteryIcon": widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon,
"showPrinterIcon": widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon
};
var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget[settingName] = value;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
@@ -635,46 +505,8 @@ Item {
setWidgetsForSection(sectionId, widgets);
return;
}
var widget = widgets[widgetIndex];
if (typeof widget === "string") {
widgets[widgetIndex] = {
"id": widget,
"enabled": true,
"minimumWidth": enabled
};
setWidgetsForSection(sectionId, widgets);
return;
}
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"minimumWidth": enabled
};
if (widget.size !== undefined)
newWidget.size = widget.size;
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex;
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.mountPath !== undefined)
newWidget.mountPath = widget.mountPath;
if (widget.showSwap !== undefined)
newWidget.showSwap = widget.showSwap;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showAudioPercent = widget.showAudioPercent ?? SettingsData.controlCenterShowAudioPercent;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showBrightnessPercent = widget.showBrightnessPercent ?? SettingsData.controlCenterShowBrightnessPercent;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showMicPercent = widget.showMicPercent ?? SettingsData.controlCenterShowMicPercent;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget.minimumWidth = enabled;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
@@ -685,141 +517,41 @@ Item {
setWidgetsForSection(sectionId, widgets);
return;
}
var widget = widgets[widgetIndex];
if (typeof widget === "string") {
widgets[widgetIndex] = {
"id": widget,
"enabled": true,
"showSwap": enabled
};
setWidgetsForSection(sectionId, widgets);
return;
}
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"showSwap": enabled
};
if (widget.size !== undefined)
newWidget.size = widget.size;
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex;
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.mountPath !== undefined)
newWidget.mountPath = widget.mountPath;
if (widget.minimumWidth !== undefined)
newWidget.minimumWidth = widget.minimumWidth;
if (widget.mediaSize !== undefined)
newWidget.mediaSize = widget.mediaSize;
if (widget.clockCompactMode !== undefined)
newWidget.clockCompactMode = widget.clockCompactMode;
if (widget.focusedWindowCompactMode !== undefined)
newWidget.focusedWindowCompactMode = widget.focusedWindowCompactMode;
if (widget.runningAppsCompactMode !== undefined)
newWidget.runningAppsCompactMode = widget.runningAppsCompactMode;
if (widget.keyboardLayoutNameCompactMode !== undefined)
newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showAudioPercent = widget.showAudioPercent ?? SettingsData.controlCenterShowAudioPercent;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showBrightnessPercent = widget.showBrightnessPercent ?? SettingsData.controlCenterShowBrightnessPercent;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showMicPercent = widget.showMicPercent ?? SettingsData.controlCenterShowMicPercent;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget.showSwap = enabled;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handleCompactModeChanged(sectionId, widgetId, value) {
var widgets = getWidgetsForSection(sectionId).slice();
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i];
var currentId = typeof widget === "string" ? widget : widget.id;
if (currentId !== widgetId)
continue;
if (typeof widget === "string") {
widgets[i] = {
"id": widget,
"enabled": true
};
widget = widgets[i];
} else {
var newWidget = {
"id": widget.id,
"enabled": widget.enabled
};
if (widget.size !== undefined)
newWidget.size = widget.size;
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex;
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.mountPath !== undefined)
newWidget.mountPath = widget.mountPath;
if (widget.minimumWidth !== undefined)
newWidget.minimumWidth = widget.minimumWidth;
if (widget.showSwap !== undefined)
newWidget.showSwap = widget.showSwap;
if (widget.mediaSize !== undefined)
newWidget.mediaSize = widget.mediaSize;
if (widget.clockCompactMode !== undefined)
newWidget.clockCompactMode = widget.clockCompactMode;
if (widget.focusedWindowCompactMode !== undefined)
newWidget.focusedWindowCompactMode = widget.focusedWindowCompactMode;
if (widget.runningAppsCompactMode !== undefined)
newWidget.runningAppsCompactMode = widget.runningAppsCompactMode;
if (widget.keyboardLayoutNameCompactMode !== undefined)
newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showAudioPercent = widget.showAudioPercent ?? SettingsData.controlCenterShowAudioPercent;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showBrightnessPercent = widget.showBrightnessPercent ?? SettingsData.controlCenterShowBrightnessPercent;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showMicPercent = widget.showMicPercent ?? SettingsData.controlCenterShowMicPercent;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
widgets[i] = newWidget;
widget = newWidget;
}
var newWidget = cloneWidgetData(widget);
switch (widgetId) {
case "music":
widget.mediaSize = value;
newWidget.mediaSize = value;
break;
case "clock":
widget.clockCompactMode = value;
newWidget.clockCompactMode = value;
break;
case "focusedWindow":
widget.focusedWindowCompactMode = value;
newWidget.focusedWindowCompactMode = value;
break;
case "runningApps":
widget.runningAppsCompactMode = value;
newWidget.runningAppsCompactMode = value;
break;
case "keyboard_layout_name":
widget.keyboardLayoutNameCompactMode = value;
newWidget.keyboardLayoutNameCompactMode = value;
break;
}
widgets[i] = newWidget;
break;
}
setWidgetsForSection(sectionId, widgets);
}
@@ -866,6 +598,8 @@ Item {
item.showBatteryIcon = widget.showBatteryIcon;
if (widget.showPrinterIcon !== undefined)
item.showPrinterIcon = widget.showPrinterIcon;
if (widget.showScreenSharingIcon !== undefined)
item.showScreenSharingIcon = widget.showScreenSharingIcon;
if (widget.minimumWidth !== undefined)
item.minimumWidth = widget.minimumWidth;
if (widget.showSwap !== undefined)
@@ -916,14 +650,28 @@ Item {
});
}
WidgetSelectionPopup {
id: widgetSelectionPopup
parentModal: widgetsTab.parentModal
onWidgetSelected: (widgetId, targetSection) => {
widgetsTab.addWidgetToSection(widgetId, targetSection);
LazyLoader {
id: widgetSelectionPopupLoader
active: false
WidgetSelectionPopup {
id: widgetSelectionPopupItem
parentModal: widgetsTab.parentModal
onWidgetSelected: (widgetId, targetSection) => {
widgetsTab.addWidgetToSection(widgetId, targetSection);
}
}
}
function showWidgetSelectionPopup(sectionId) {
widgetSelectionPopupLoader.active = true;
if (!widgetSelectionPopupLoader.item)
return;
widgetSelectionPopupLoader.item.targetSection = sectionId;
widgetSelectionPopupLoader.item.allWidgets = widgetsTab.getWidgetsForPopup();
widgetSelectionPopupLoader.item.show();
}
DankFlickable {
anchors.fill: parent
clip: true
@@ -1113,9 +861,7 @@ Item {
widgetsTab.handleItemOrderChanged(sectionId, newOrder);
}
onAddWidget: sectionId => {
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.allWidgets = widgetsTab.getWidgetsForPopup();
widgetSelectionPopup.show();
showWidgetSelectionPopup(sectionId);
}
onRemoveWidget: (sectionId, index) => {
widgetsTab.removeWidgetFromSection(sectionId, index);
@@ -1170,9 +916,7 @@ Item {
widgetsTab.handleItemOrderChanged(sectionId, newOrder);
}
onAddWidget: sectionId => {
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.allWidgets = widgetsTab.getWidgetsForPopup();
widgetSelectionPopup.show();
showWidgetSelectionPopup(sectionId);
}
onRemoveWidget: (sectionId, index) => {
widgetsTab.removeWidgetFromSection(sectionId, index);
@@ -1227,9 +971,7 @@ Item {
widgetsTab.handleItemOrderChanged(sectionId, newOrder);
}
onAddWidget: sectionId => {
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.allWidgets = widgetsTab.getWidgetsForPopup();
widgetSelectionPopup.show();
showWidgetSelectionPopup(sectionId);
}
onRemoveWidget: (sectionId, index) => {
widgetsTab.removeWidgetFromSection(sectionId, index);

View File

@@ -31,6 +31,19 @@ Column {
signal minimumWidthChanged(string sectionId, int widgetIndex, bool enabled)
signal showSwapChanged(string sectionId, int widgetIndex, bool enabled)
function cloneWidgetData(widget) {
var result = {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "minimumWidth", "showSwap", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon"];
for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
}
return result;
}
width: parent.width
height: implicitHeight
spacing: Theme.spacingM
@@ -727,13 +740,7 @@ Column {
var newItems = root.items.slice();
var draggedItem = newItems.splice(index, 1)[0];
newItems.splice(newIndex, 0, draggedItem);
root.itemOrderChanged(newItems.map(item => {
return ({
"id": item.id,
"enabled": item.enabled,
"size": item.size
});
}));
root.itemOrderChanged(newItems.map(item => root.cloneWidgetData(item)));
}
}
delegateItem.x = 0;
@@ -875,6 +882,11 @@ Column {
icon: "print",
label: I18n.tr("Printer"),
setting: "showPrinterIcon"
},
{
icon: "screen_record",
label: I18n.tr("Screen Sharing"),
setting: "showScreenSharingIcon"
}
]
@@ -907,6 +919,8 @@ Column {
return wd?.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
case "showPrinterIcon":
return wd?.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
case "showScreenSharingIcon":
return wd?.showScreenSharingIcon ?? SettingsData.controlCenterShowScreenSharingIcon;
default:
return false;
}

View File

@@ -63,15 +63,15 @@ Item {
onToggled: checked => SettingsData.set("showWorkspaceApps", checked)
}
Row {
width: parent.width - Theme.spacingL
spacing: Theme.spacingL
Item {
width: parent.width
height: maxAppsColumn.height
visible: SettingsData.showWorkspaceApps
opacity: visible ? 1 : 0
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
Column {
id: maxAppsColumn
x: Theme.spacingL
width: 120
spacing: Theme.spacingS
@@ -80,14 +80,15 @@ Item {
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignLeft
}
DankTextField {
width: 100
height: 28
placeholderText: "#ffffff"
placeholderText: "3"
text: SettingsData.maxWorkspaceIcons
maximumLength: 7
maximumLength: 2
font.pixelSize: Theme.fontSizeSmall
topPadding: Theme.spacingXS
bottomPadding: Theme.spacingXS

View File

@@ -11,7 +11,7 @@ Singleton {
id: root
readonly property string currentVersion: "1.2"
readonly property bool changelogEnabled: true
readonly property bool changelogEnabled: false
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion

View File

@@ -21,6 +21,7 @@ Singleton {
property int processLimit: 20
property string processSort: "cpu"
property bool noCpu: false
property int dgopProcessPid: 0
// Cursor data for accurate CPU calculations
property string cpuCursor: ""
@@ -59,6 +60,7 @@ Singleton {
property var processes: []
property var allProcesses: []
property string currentSort: "cpu"
property bool sortAscending: false
property var availableGpus: []
property string kernelVersion: ""
@@ -93,10 +95,8 @@ Singleton {
if (modules) {
const modulesToAdd = Array.isArray(modules) ? modules : [modules];
for (const module of modulesToAdd) {
// Increment reference count for this module
const currentCount = moduleRefCounts[module] || 0;
moduleRefCounts[module] = currentCount + 1;
console.log("Adding ref for module:", module, "count:", moduleRefCounts[module]);
// Add to enabled modules if not already there
if (enabledModules.indexOf(module) === -1) {
@@ -126,17 +126,13 @@ Singleton {
for (const module of modulesToRemove) {
const currentCount = moduleRefCounts[module] || 0;
if (currentCount > 1) {
// Decrement reference count
moduleRefCounts[module] = currentCount - 1;
console.log("Removing ref for module:", module, "count:", moduleRefCounts[module]);
} else if (currentCount === 1) {
// Remove completely when count reaches 0
delete moduleRefCounts[module];
const index = enabledModules.indexOf(module);
if (index > -1) {
enabledModules.splice(index, 1);
modulesChanged = true;
console.log("Disabling module:", module, "(no more refs)");
}
}
}
@@ -171,17 +167,13 @@ Singleton {
gpuPciIds = gpuPciIds.concat([pciId]);
}
console.log("Adding GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]);
// Force property change notification
gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts);
}
function removeGpuPciId(pciId) {
const currentCount = gpuPciIdRefCounts[pciId] || 0;
if (currentCount > 1) {
// Decrement reference count
gpuPciIdRefCounts[pciId] = currentCount - 1;
console.log("Removing GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]);
} else if (currentCount === 1) {
// Remove completely when count reaches 0
delete gpuPciIdRefCounts[pciId];
@@ -203,8 +195,6 @@ Singleton {
}
availableGpus = updatedGpus;
}
console.log("Removing GPU PCI ID completely:", pciId);
}
// Force property change notification
@@ -389,8 +379,12 @@ Singleton {
if (data.processes && Array.isArray(data.processes)) {
const newProcesses = [];
processSampleCount++;
const ourPid = dgopProcessPid;
for (const proc of data.processes) {
if (ourPid > 0 && proc.pid === ourPid)
continue;
const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0;
newProcesses.push({
@@ -577,39 +571,55 @@ Singleton {
function setSortBy(newSortBy) {
if (newSortBy !== currentSort) {
currentSort = newSortBy;
sortAscending = false;
applySorting();
}
}
function applySorting() {
if (!allProcesses || allProcesses.length === 0) {
return;
function toggleSort(column) {
if (column === currentSort) {
sortAscending = !sortAscending;
} else {
currentSort = column;
sortAscending = false;
}
applySorting();
}
function applySorting() {
if (!allProcesses || allProcesses.length === 0)
return;
const asc = sortAscending;
const sorted = allProcesses.slice();
sorted.sort((a, b) => {
let valueA, valueB;
let valueA, valueB, result;
switch (currentSort) {
case "cpu":
valueA = a.cpu || 0;
valueB = b.cpu || 0;
return valueB - valueA;
result = valueB - valueA;
break;
case "memory":
valueA = a.memoryKB || 0;
valueB = b.memoryKB || 0;
return valueB - valueA;
result = valueB - valueA;
break;
case "name":
valueA = (a.command || "").toLowerCase();
valueB = (b.command || "").toLowerCase();
return valueA.localeCompare(valueB);
result = valueA.localeCompare(valueB);
break;
case "pid":
valueA = a.pid || 0;
valueB = b.pid || 0;
return valueA - valueB;
result = valueA - valueB;
break;
default:
return 0;
}
return asc ? -result : result;
});
processes = sorted.slice(0, processLimit);
@@ -628,10 +638,7 @@ Singleton {
id: dgopProcess
command: root.buildDgopCommand()
running: false
onCommandChanged:
//console.log("DgopService command:", JSON.stringify(command))
{}
onStarted: dgopProcessPid = processId ?? 0
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("Dgop process failed with exit code:", exitCode);

View File

@@ -27,6 +27,10 @@ Singleton {
property bool inOverview: false
property var casts: []
property bool hasCasts: casts.length > 0
property bool hasActiveCast: casts.some(c => c.is_active)
property int currentKeyboardLayoutIndex: 0
property var keyboardLayoutNames: []
@@ -356,6 +360,15 @@ Singleton {
case 'ScreenshotCaptured':
handleScreenshotCaptured(event.ScreenshotCaptured);
break;
case 'CastsChanged':
handleCastsChanged(event.CastsChanged);
break;
case 'CastStartedOrChanged':
handleCastStartedOrChanged(event.CastStartedOrChanged);
break;
case 'CastStopped':
handleCastStopped(event.CastStopped);
break;
}
}
@@ -649,6 +662,28 @@ Singleton {
}
}
function handleCastsChanged(data) {
casts = data.casts || [];
}
function handleCastStartedOrChanged(data) {
if (!data.cast)
return;
const cast = data.cast;
const existingIndex = casts.findIndex(c => c.stream_id === cast.stream_id);
if (existingIndex >= 0) {
const updatedCasts = [...casts];
updatedCasts[existingIndex] = cast;
casts = updatedCasts;
} else {
casts = [...casts, cast];
}
}
function handleCastStopped(data) {
casts = casts.filter(c => c.stream_id !== data.stream_id);
}
function updateCurrentOutputWorkspaces() {
if (!currentOutput) {
currentOutputWorkspaces = allWorkspaces;

View File

@@ -287,11 +287,14 @@ Singleton {
return false;
}
// MODIFICATION: Treat Launchers as persistent instances like Daemons
if (isDaemon || isLauncher) {
if (isDaemon) {
const newDaemons = Object.assign({}, pluginDaemonComponents);
newDaemons[pluginId] = comp;
pluginDaemonComponents = newDaemons;
} else if (isLauncher) {
const instance = comp.createObject(root, {
"pluginId": pluginId,
"pluginService": root // Inject PluginService
"pluginService": root
});
if (!instance) {
console.error("PluginService: failed to instantiate plugin:", pluginId, comp.errorString());
@@ -302,15 +305,9 @@ Singleton {
newInstances[pluginId] = instance;
pluginInstances = newInstances;
if (isDaemon) {
const newDaemons = Object.assign({}, pluginDaemonComponents);
newDaemons[pluginId] = comp;
pluginDaemonComponents = newDaemons;
} else {
const newLaunchers = Object.assign({}, pluginLauncherComponents);
newLaunchers[pluginId] = comp;
pluginLauncherComponents = newLaunchers;
}
const newLaunchers = Object.assign({}, pluginLauncherComponents);
newLaunchers[pluginId] = comp;
pluginLauncherComponents = newLaunchers;
} else if (isDesktop) {
const newDesktop = Object.assign({}, pluginDesktopComponents);
newDesktop[pluginId] = comp;

View File

@@ -27,7 +27,9 @@ Singleton {
property var colorPickerModal: null
property var notificationModal: null
property var wifiPasswordModal: null
property var wifiPasswordModalLoader: null
property var polkitAuthModal: null
property var polkitAuthModalLoader: null
property var bluetoothPairingModal: null
property var networkInfoModal: null
@@ -416,11 +418,17 @@ Singleton {
}
function showWifiPasswordModal(ssid) {
wifiPasswordModal?.show(ssid);
if (wifiPasswordModalLoader)
wifiPasswordModalLoader.active = true;
if (wifiPasswordModal)
wifiPasswordModal.show(ssid);
}
function showHiddenNetworkModal() {
wifiPasswordModal?.showHidden();
if (wifiPasswordModalLoader)
wifiPasswordModalLoader.active = true;
if (wifiPasswordModal)
wifiPasswordModal.showHidden();
}
function hideWifiPasswordModal() {

View File

@@ -754,7 +754,7 @@ Singleton {
"humidity": Math.round(hourly.relative_humidity_2m?.[i] || 0),
"wind": Math.round(hourly.wind_speed_10m?.[i] || 0),
"pressure": Math.round(hourly.surface_pressure?.[i] || 0),
"precipitationProbability": Math.round(hourly.precipitation_probability_max?.[0] || 0),
"precipitationProbability": Math.round(hourly.precipitation_probability?.[i] || 0),
"visibility": Math.round(hourly.visibility?.[i] || 0),
"isDay": isDay
});

View File

@@ -1 +1 @@
v1.2.0
v1.4-unstable

View File

@@ -202,7 +202,7 @@
{
"scope": ["keyword"],
"settings": {
"foreground": "{{dank16.color5.dark.hex}}"
"foreground": "{{dank16.color6.dark.hex}}"
}
},
{
@@ -320,7 +320,7 @@
"foreground": "{{dank16.color12.dark.hex}}"
},
"keyword": {
"foreground": "{{dank16.color5.dark.hex}}"
"foreground": "{{dank16.color6.dark.hex}}"
},
"comment": {
"foreground": "{{dank16.color8.dark.hex}}"

View File

@@ -148,7 +148,7 @@
{
"scope": ["keyword"],
"settings": {
"foreground": "{{dank16.color5.default.hex}}"
"foreground": "{{dank16.color6.default.hex}}"
}
},
{
@@ -272,7 +272,7 @@
"foreground": "{{dank16.color12.default.hex}}"
},
"keyword": {
"foreground": "{{dank16.color5.default.hex}}"
"foreground": "{{dank16.color6.default.hex}}"
},
"comment": {
"foreground": "{{colors.outline.default.hex}}"

View File

@@ -190,7 +190,7 @@
"storage.type"
],
"settings": {
"foreground": "{{dank16.color5.light.hex}}"
"foreground": "{{dank16.color6.light.hex}}"
}
},
{
@@ -444,7 +444,7 @@
"foreground": "{{colors.outline.light.hex}}"
},
"keyword": {
"foreground": "{{dank16.color5.light.hex}}"
"foreground": "{{dank16.color6.light.hex}}"
},
"operator": {
"foreground": "{{colors.on_surface.light.hex}}"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -240,7 +240,7 @@
"All displays": "Tutti gli schermi"
},
"Allow clicks to pass through the widget": {
"Allow clicks to pass through the widget": ""
"Allow clicks to pass through the widget": "Consenti il passaggio dei clic attraverso il widget"
},
"Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close": {
"Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close": "Alt+←/Backspace: Indietro • F1/I: File Info • F10: Aiuto • Esc: Chiudi"
@@ -279,7 +279,7 @@
"Anonymous Identity (optional)": "Identità anonima (facoltativa)"
},
"App ID Substitutions": {
"App ID Substitutions": ""
"App ID Substitutions": "Sostituzioni App ID"
},
"App Launcher": {
"App Launcher": "App Launcher"
@@ -729,7 +729,7 @@
"Click Import to add a .ovpn or .conf": "Clicca su Importa per aggiungere un file .ovpn o .conf"
},
"Click Through": {
"Click Through": ""
"Click Through": "Clic passanti"
},
"Click any shortcut to edit. Changes save to %1": {
"Click any shortcut to edit. Changes save to %1": "Clicca su qualsiasi scorciatoia per modificare. Le modifiche vengono salvate in %1"
@@ -1797,7 +1797,7 @@
"Grid Columns": "Colonne Griglia"
},
"Group": {
"Group": ""
"Group": "Gruppo"
},
"Group Workspace Apps": {
"Group Workspace Apps": "Raggruppa App per Spazio di Lavoro"
@@ -1809,13 +1809,13 @@
"Group multiple windows of the same app together with a window count indicator": "Raggruppa molteplici finestre della stessa app con un indicatore del numero di finestre"
},
"Group removed": {
"Group removed": ""
"Group removed": "Gruppo rimosso"
},
"Group repeated application icons in unfocused workspaces": {
"Group repeated application icons in unfocused workspaces": "Raggruppa le icone delle applicazioni duplicate negli spazi di lavoro non attivi"
},
"Groups": {
"Groups": ""
"Groups": "Gruppi"
},
"HDR (EDID)": {
"HDR (EDID)": "HDR (EDID)"
@@ -2214,7 +2214,7 @@
"Manual Show/Hide": "Mostra/Nascondi Manuale"
},
"Map window class names to icon names for proper icon display": {
"Map window class names to icon names for proper icon display": ""
"Map window class names to icon names for proper icon display": "Associa i nomi delle classi delle finestre ai nomi delle icone per una corretta visualizzazione"
},
"Margin": {
"Margin": "Margini"
@@ -2439,7 +2439,7 @@
"New York, NY": "New York, NY"
},
"New group name...": {
"New group name...": ""
"New group name...": "Nome del nuovo gruppo..."
},
"Next Transition": {
"Next Transition": "Prossima Transizione"
@@ -2673,7 +2673,7 @@
"Options": "Opzioni"
},
"Organize widgets into collapsible groups": {
"Organize widgets into collapsible groups": ""
"Organize widgets into collapsible groups": "Organizza i widget in gruppi comprimibili"
},
"Other": {
"Other": "Altro"
@@ -2748,7 +2748,7 @@
"Password": "Password"
},
"Pattern": {
"Pattern": ""
"Pattern": "Pattern"
},
"Pause": {
"Pause": "Pausa"
@@ -3012,7 +3012,7 @@
"Repeat": "Ripetizione"
},
"Replacement": {
"Replacement": ""
"Replacement": "Sostituzione"
},
"Report": {
"Report": "Riepilogo"
@@ -3648,7 +3648,7 @@
"Sync Mode with Portal": "Modalità Sync con Portale"
},
"Sync Position Across Screens": {
"Sync Position Across Screens": ""
"Sync Position Across Screens": "Sincronizza la posizione tra gli schermi"
},
"Sync dark mode with settings portals for system-wide theme hints": {
"Sync dark mode with settings portals for system-wide theme hints": "Sincronizza tema scuro con impostazioni di sistema"
@@ -3876,7 +3876,7 @@
"Unfocused Color": "Colore Inattivo"
},
"Ungrouped": {
"Ungrouped": ""
"Ungrouped": "Non Raggruppato"
},
"Uninstall Plugin": {
"Uninstall Plugin": "Disinstalla Plugin"
@@ -3978,13 +3978,13 @@
"Use light theme instead of dark theme": "Usa tema chiaro invece del tema scuro"
},
"Use smaller notification cards": {
"Use smaller notification cards": ""
"Use smaller notification cards": "Usa schede di notifica più piccole"
},
"Use sound theme from system settings": {
"Use sound theme from system settings": "Usa tema di suoni dalle impostazioni di sistema"
},
"Use the same position and size on all displays": {
"Use the same position and size on all displays": ""
"Use the same position and size on all displays": "Usa la stessa posizione e dimensione su tutti gli schermi"
},
"Use trigger prefix to activate": {
"Use trigger prefix to activate": "Usa il prefisso attivatore per attivare"
@@ -4553,7 +4553,7 @@
"No wallpaper selected": "Nessuno sfondo selezionato"
},
"notification center tab": {
"Current": "Attuale",
"Current": "Attuali",
"History": "Cronologia"
},
"notification history filter": {

View File

@@ -240,7 +240,7 @@
"All displays": "所有显示器"
},
"Allow clicks to pass through the widget": {
"Allow clicks to pass through the widget": ""
"Allow clicks to pass through the widget": "允许鼠标穿透部件"
},
"Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close": {
"Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close": "Alt+←/退格: 返回 • F1/I: 文件信息 • F10: 帮助 • Esc: 关闭"
@@ -279,7 +279,7 @@
"Anonymous Identity (optional)": "匿名身份(可选)"
},
"App ID Substitutions": {
"App ID Substitutions": ""
"App ID Substitutions": "应用ID替换"
},
"App Launcher": {
"App Launcher": "启动器"
@@ -729,7 +729,7 @@
"Click Import to add a .ovpn or .conf": "点击导入添加 .ovpn 或 .conf 文件"
},
"Click Through": {
"Click Through": ""
"Click Through": "鼠标穿透"
},
"Click any shortcut to edit. Changes save to %1": {
"Click any shortcut to edit. Changes save to %1": "点击任意快捷方式以编辑。更改将保存至%1。"
@@ -1797,7 +1797,7 @@
"Grid Columns": "网格列"
},
"Group": {
"Group": ""
"Group": "分组"
},
"Group Workspace Apps": {
"Group Workspace Apps": "分组工作区应用"
@@ -1809,13 +1809,13 @@
"Group multiple windows of the same app together with a window count indicator": "将同一应用的多个窗口合并显示,并标注窗口数量"
},
"Group removed": {
"Group removed": ""
"Group removed": "分组已移除"
},
"Group repeated application icons in unfocused workspaces": {
"Group repeated application icons in unfocused workspaces": "在不聚焦的工作区中将重复应用图标分组"
},
"Groups": {
"Groups": ""
"Groups": "分组"
},
"HDR (EDID)": {
"HDR (EDID)": "HDREDID"
@@ -2214,7 +2214,7 @@
"Manual Show/Hide": "手动显示/隐藏"
},
"Map window class names to icon names for proper icon display": {
"Map window class names to icon names for proper icon display": ""
"Map window class names to icon names for proper icon display": "将窗口类名称映射到图标名称以实现正确的图标显示"
},
"Margin": {
"Margin": "边距"
@@ -2439,7 +2439,7 @@
"New York, NY": "纽约,美国纽约州"
},
"New group name...": {
"New group name...": ""
"New group name...": "新分组名..."
},
"Next Transition": {
"Next Transition": "下一过渡"
@@ -2673,7 +2673,7 @@
"Options": "选项"
},
"Organize widgets into collapsible groups": {
"Organize widgets into collapsible groups": ""
"Organize widgets into collapsible groups": "将部件组织成可折叠分组"
},
"Other": {
"Other": "其他"
@@ -2748,7 +2748,7 @@
"Password": "密码"
},
"Pattern": {
"Pattern": ""
"Pattern": "模式"
},
"Pause": {
"Pause": "暂停"
@@ -3012,7 +3012,7 @@
"Repeat": "重复"
},
"Replacement": {
"Replacement": ""
"Replacement": "替换"
},
"Report": {
"Report": "报告"
@@ -3648,7 +3648,7 @@
"Sync Mode with Portal": "同步系统深色模式"
},
"Sync Position Across Screens": {
"Sync Position Across Screens": ""
"Sync Position Across Screens": "在显示器间同步位置"
},
"Sync dark mode with settings portals for system-wide theme hints": {
"Sync dark mode with settings portals for system-wide theme hints": "随系统设置开启深色模式,以适配全局主题"
@@ -3876,7 +3876,7 @@
"Unfocused Color": "未聚焦颜色"
},
"Ungrouped": {
"Ungrouped": ""
"Ungrouped": "已解除分组"
},
"Uninstall Plugin": {
"Uninstall Plugin": "卸载插件"
@@ -3978,13 +3978,13 @@
"Use light theme instead of dark theme": "使用浅色主题替代深色主题"
},
"Use smaller notification cards": {
"Use smaller notification cards": ""
"Use smaller notification cards": "使用更小的通知卡"
},
"Use sound theme from system settings": {
"Use sound theme from system settings": "使用系统设置中的声音主题"
},
"Use the same position and size on all displays": {
"Use the same position and size on all displays": ""
"Use the same position and size on all displays": "在所有显示器上使用同样的位置与大小"
},
"Use trigger prefix to activate": {
"Use trigger prefix to activate": "使用触发前缀以激活"