1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-28 05:55:21 -04:00

Compare commits

..

6 Commits

Author SHA1 Message Date
purian23 aed731efb0 fix(clipboard): restore Save button targets in editor 2026-05-25 23:19:42 -04:00
purian23 cf0632c077 feat(Clipboard): Revive ClipboardEditor PR
- Original PR #1916 by @nabaco
2026-05-24 23:28:21 -04:00
Nachum Barcohen e92da4a15f Show full clipboard text in editor 2026-05-24 22:34:24 -04:00
Nachum Barcohen 8abdff3220 Add clipboard editor shortcuts and hints 2026-05-24 22:34:24 -04:00
Nachum Barcohen 584d57a8de Add split save menu for clipboard editor 2026-05-24 22:34:05 -04:00
Nachum Barcohen afb5e59c29 feat(clipboard): Add editing capability to clipboard entries 2026-05-24 22:34:05 -04:00
58 changed files with 2375 additions and 13159 deletions
+1 -1
View File
@@ -26,4 +26,4 @@ jobs:
go-version-file: core/go.mod go-version-file: core/go.mod
- name: run pre-commit hooks - name: run pre-commit hooks
uses: j178/prek-action@v2 uses: j178/prek-action@v1
+1 -4
View File
@@ -947,12 +947,9 @@ func checkSystemdServices() []checkResult {
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active) message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
} }
switch { switch {
case dmsState.active == "failed":
status = statusError
case dmsState.active == "active":
case dmsState.enabled == "disabled": case dmsState.enabled == "disabled":
status, message = statusWarn, "Disabled" status, message = statusWarn, "Disabled"
case dmsState.active == "inactive": case dmsState.active == "failed" || dmsState.active == "inactive":
status = statusError status = statusError
} }
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"}) results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
+10 -52
View File
@@ -59,29 +59,22 @@ var greeterInstallCmd = &cobra.Command{
} }
var greeterSyncCmd = &cobra.Command{ var greeterSyncCmd = &cobra.Command{
Use: "sync", Use: "sync",
Short: "Sync DMS theme and settings with greeter", Short: "Sync DMS theme and settings with greeter",
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen. Also updates a per-user cache slot at users/<username>/ for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users/<username>/ slot without sudo or greetd changes.", Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: preRunPrivileged,
profile, _ := cmd.Flags().GetBool("profile")
if profile {
return nil
}
return preRunPrivileged(cmd, args)
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes") yes, _ := cmd.Flags().GetBool("yes")
auth, _ := cmd.Flags().GetBool("auth") auth, _ := cmd.Flags().GetBool("auth")
local, _ := cmd.Flags().GetBool("local") local, _ := cmd.Flags().GetBool("local")
profile, _ := cmd.Flags().GetBool("profile")
term, _ := cmd.Flags().GetBool("terminal") term, _ := cmd.Flags().GetBool("terminal")
if term { if term {
if err := syncInTerminal(yes, auth, local, profile); err != nil { if err := syncInTerminal(yes, auth, local); err != nil {
log.Fatalf("Error launching sync in terminal: %v", err) log.Fatalf("Error launching sync in terminal: %v", err)
} }
return return
} }
if err := syncGreeter(yes, auth, local, profile); err != nil { if err := syncGreeter(yes, auth, local); err != nil {
log.Fatalf("Error syncing greeter: %v", err) log.Fatalf("Error syncing greeter: %v", err)
} }
}, },
@@ -92,7 +85,6 @@ func init() {
greeterSyncCmd.Flags().BoolP("terminal", "t", false, "Run sync in a new terminal (for entering sudo password); terminal auto-closes when done") greeterSyncCmd.Flags().BoolP("terminal", "t", false, "Run sync in a new terminal (for entering sudo password); terminal auto-closes when done")
greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles") greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles")
greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path") greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path")
greeterSyncCmd.Flags().BoolP("profile", "p", false, "Sync only your per-user greeter slot (no sudo; for secondary accounts)")
} }
var greeterEnableCmd = &cobra.Command{ var greeterEnableCmd = &cobra.Command{
@@ -520,8 +512,8 @@ func runCommandInTerminal(shellCmd string) error {
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)") return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
} }
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly bool) error { func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
syncFlags := make([]string, 0, 4) syncFlags := make([]string, 0, 3)
if nonInteractive { if nonInteractive {
syncFlags = append(syncFlags, "--yes") syncFlags = append(syncFlags, "--yes")
} }
@@ -531,9 +523,6 @@ func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly
if local { if local {
syncFlags = append(syncFlags, "--local") syncFlags = append(syncFlags, "--local")
} }
if profileOnly {
syncFlags = append(syncFlags, "--profile")
}
shellSyncCmd := "dms greeter sync" shellSyncCmd := "dms greeter sync"
if len(syncFlags) > 0 { if len(syncFlags) > 0 {
shellSyncCmd += " " + strings.Join(syncFlags, " ") shellSyncCmd += " " + strings.Join(syncFlags, " ")
@@ -552,11 +541,7 @@ func resolveLocalWrapperShell() (string, error) {
return "", fmt.Errorf("could not find bash or sh in PATH for local greeter wrapper") return "", fmt.Errorf("could not find bash or sh in PATH for local greeter wrapper")
} }
func syncGreeter(nonInteractive bool, forceAuth bool, local bool, profileOnly bool) error { func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
if profileOnly {
return syncGreeterProfileOnly(nonInteractive)
}
if !nonInteractive { if !nonInteractive {
fmt.Println("=== DMS Greeter Sync ===") fmt.Println("=== DMS Greeter Sync ===")
fmt.Println() fmt.Println()
@@ -767,26 +752,6 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool, profileOnly bo
return nil return nil
} }
func syncGreeterProfileOnly(nonInteractive bool) error {
logFunc := func(msg string) {
fmt.Println(msg)
}
if !nonInteractive {
fmt.Println("=== DMS Greeter Profile Sync ===")
fmt.Println()
fmt.Println("Syncing your personal greeter theme slot (no system changes)...")
}
if err := greeter.SyncUserProfileCache(logFunc); err != nil {
return err
}
if !nonInteractive {
fmt.Println("\n=== Profile Sync Complete ===")
fmt.Println("\nYour theme, wallpaper, and profile photo have been synced for the login screen.")
fmt.Println("Log out to preview your greeter look when selecting your account.")
}
return nil
}
func hasDmsShellQml(dir string) bool { func hasDmsShellQml(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "shell.qml")) info, err := os.Stat(filepath.Join(dir, "shell.qml"))
return err == nil && !info.IsDir() return err == nil && !info.IsDir()
@@ -872,14 +837,7 @@ func resolveLocalDMSPath() (string, error) {
} }
} }
configuredCommand := readDefaultSessionCommand("/etc/greetd/config.toml") return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root or set DMS_LOCAL_PATH=/absolute/path/to/repo", wd)
if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" {
if resolved, ok := resolveDMSLocalCandidate(pathOverride); ok {
return resolved, nil
}
}
return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root, set DMS_LOCAL_PATH=/absolute/path/to/repo, or configure greetd with -p /path/to/quickshell", wd)
} }
func disableDisplayManager(dmName string) (bool, error) { func disableDisplayManager(dmName string) (bool, error) {
-12
View File
@@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@@ -573,7 +572,6 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
} }
runtimeDirs := []string{ runtimeDirs := []string{
filepath.Join(cacheDir, "users"),
filepath.Join(cacheDir, ".local"), filepath.Join(cacheDir, ".local"),
filepath.Join(cacheDir, ".local", "state"), filepath.Join(cacheDir, ".local", "state"),
filepath.Join(cacheDir, ".local", "share"), filepath.Join(cacheDir, ".local", "share"),
@@ -1257,16 +1255,6 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
return fmt.Errorf("greeter wallpaper override sync failed: %w", err) return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
} }
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to resolve syncing user for per-user greeter cache: %w", err)
}
if err := syncUserGreeterCacheSlot(homeDir, cacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{
sudoPassword: sudoPassword,
}); err != nil {
return fmt.Errorf("per-user greeter cache sync failed: %w", err)
}
if strings.ToLower(compositor) != "niri" { if strings.ToLower(compositor) != "niri" {
return nil return nil
} }
-548
View File
@@ -1,548 +0,0 @@
package greeter
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
var monitorWallpaperSanitizer = regexp.MustCompile(`[^a-zA-Z0-9]+`)
func userGreeterCacheDir(cacheDir, username string) string {
return filepath.Join(cacheDir, "users", username)
}
func isUserOwnedGreeterCacheSlot(path, username string) bool {
if strings.TrimSpace(username) == "" {
return false
}
userDir, err := filepath.Abs(userGreeterCacheDir(GreeterCacheDir, username))
if err != nil {
return false
}
abs, err := filepath.Abs(path)
if err != nil {
return false
}
return abs == userDir || strings.HasPrefix(abs, userDir+string(filepath.Separator))
}
func UserIsInGreeterGroup(username string) bool {
group := DetectGreeterGroup()
if !utils.HasGroup(group) {
return false
}
groupsCmd := exec.Command("groups", username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return false
}
return strings.Contains(string(groupsOutput), group)
}
func CanSyncOwnUserGreeterProfile(username string) bool {
currentUser, err := user.Current()
if err != nil || currentUser.Username != username {
return false
}
if !UserIsInGreeterGroup(username) {
return false
}
usersDir := filepath.Join(GreeterCacheDir, "users")
if st, err := os.Stat(usersDir); err != nil || !st.IsDir() {
return false
}
testFile := filepath.Join(usersDir, ".write-test-"+username)
file, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
if err != nil {
return false
}
_ = file.Close()
_ = os.Remove(testFile)
return true
}
func GreeterProfileSyncReady() bool {
if command := readGreeterSessionCommand(); command != "" && strings.Contains(command, "dms-greeter") {
return true
}
usersDir := filepath.Join(GreeterCacheDir, "users")
st, err := os.Stat(usersDir)
return err == nil && st.IsDir()
}
func readGreeterSessionCommand() string {
data, err := os.ReadFile("/etc/greetd/config.toml")
if err != nil {
return ""
}
inDefaultSession := false
for line := range strings.SplitSeq(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
inDefaultSession = strings.EqualFold(strings.Trim(trimmed, "[]"), "default_session")
continue
}
if !inDefaultSession {
continue
}
if idx := strings.Index(trimmed, "#"); idx >= 0 {
trimmed = strings.TrimSpace(trimmed[:idx])
}
if !strings.HasPrefix(trimmed, "command") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
if command != "" {
return command
}
}
return ""
}
// SyncUserProfileCache writes the current user's theme slot under users/<username>/
// without modifying greetd or other system configuration. Requires membership in the
// greeter group and a prior full greeter setup by an administrator.
func SyncUserProfileCache(logFunc func(string)) error {
if logFunc == nil {
logFunc = func(string) {}
}
if !GreeterProfileSyncReady() {
return fmt.Errorf("greeter is not set up on this system yet; an administrator must run 'dms greeter install' or 'dms greeter sync' once first")
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to resolve current user: %w", err)
}
if !CanSyncOwnUserGreeterProfile(currentUser.Username) {
group := DetectGreeterGroup()
return fmt.Errorf("cannot sync greeter profile: you must be in the %s group with write access to %s/users\nAsk an administrator to run:\n sudo usermod -aG %s %s\nThen log out and back in before running:\n dms greeter sync --profile",
group, GreeterCacheDir, group, currentUser.Username)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
return fmt.Errorf("failed to resolve greeter color source: %w", err)
}
if err := syncUserGreeterCacheSlot(homeDir, GreeterCacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{
profileOnly: true,
}); err != nil {
return err
}
logFunc(fmt.Sprintf(" → %s/users/%s/", GreeterCacheDir, currentUser.Username))
return nil
}
func canWriteUserGreeterCacheSlot(dest, username string) bool {
return isUserOwnedGreeterCacheSlot(dest, username) && CanSyncOwnUserGreeterProfile(username)
}
type userSlotSyncOpts struct {
sudoPassword string
profileOnly bool
username string
}
func (o userSlotSyncOpts) useDirectWrite(dest string) bool {
if !o.profileOnly {
return false
}
return canWriteUserGreeterCacheSlot(dest, o.username)
}
func isGreeterCachePath(path string) bool {
abs, err := filepath.Abs(path)
if err != nil {
return true
}
cacheAbs, err := filepath.Abs(GreeterCacheDir)
if err != nil {
return true
}
if abs == cacheAbs {
return true
}
return strings.HasPrefix(abs, cacheAbs+string(filepath.Separator))
}
func greeterCacheOwner() string {
greeterGroup := DetectGreeterGroup()
daemonUser := DetectGreeterUser()
return daemonUser + ":" + greeterGroup
}
func ensureGreeterCacheSubdir(dir string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dir) {
if err := os.MkdirAll(dir, 0o770); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
return nil
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "mkdir", "-p", dir); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), opts.sudoPassword, "chown", owner, dir); err != nil {
if fallbackErr := privesc.Run(context.Background(), opts.sudoPassword, "chown", "root:"+DetectGreeterGroup(), dir); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", dir, err)
}
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "chmod", "2770", dir); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", dir, err)
}
return nil
}
func setGreeterCacheFileOwnership(path, sudoPassword string) error {
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, path); err != nil {
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+DetectGreeterGroup(), path); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", path, err)
}
}
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", path); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", path, err)
}
return nil
}
func syncUserGreeterCacheSlot(homeDir, cacheDir, username string, state greeterThemeSyncState, logFunc func(string), opts userSlotSyncOpts) error {
if strings.TrimSpace(username) == "" {
return nil
}
opts.username = username
userDir := userGreeterCacheDir(cacheDir, username)
if err := ensureGreeterCacheSubdir(userDir, opts); err != nil {
return err
}
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
settingsBytes, err := os.ReadFile(settingsPath)
if err != nil {
return fmt.Errorf("failed to read settings for user cache slot: %w", err)
}
settingsMap := map[string]any{}
if strings.TrimSpace(string(settingsBytes)) != "" {
if err := json.Unmarshal(settingsBytes, &settingsMap); err != nil {
return fmt.Errorf("failed to parse settings for user cache slot: %w", err)
}
}
if customTheme, ok := settingsMap["customThemeFile"].(string); ok && strings.TrimSpace(customTheme) != "" {
resolvedTheme := customTheme
if !filepath.IsAbs(resolvedTheme) {
resolvedTheme = filepath.Join(homeDir, resolvedTheme)
}
if st, statErr := os.Stat(resolvedTheme); statErr == nil && !st.IsDir() {
destTheme := filepath.Join(userDir, "custom-theme.json")
if err := copyFileWithPrivesc(resolvedTheme, destTheme, opts); err != nil {
return err
}
settingsMap["customThemeFile"] = destTheme
}
}
settingsBytes, err = json.Marshal(settingsMap)
if err != nil {
return fmt.Errorf("failed to marshal settings for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "settings.json"), settingsBytes, opts); err != nil {
return err
}
sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json")
sessionBytes, err := os.ReadFile(sessionPath)
if err != nil {
return fmt.Errorf("failed to read session for user cache slot: %w", err)
}
sessionMap := map[string]any{}
if strings.TrimSpace(string(sessionBytes)) != "" {
if err := json.Unmarshal(sessionBytes, &sessionMap); err != nil {
return fmt.Errorf("failed to parse session for user cache slot: %w", err)
}
}
if err := localizeSessionWallpapers(sessionMap, userDir, opts); err != nil {
return err
}
sessionBytes, err = json.Marshal(sessionMap)
if err != nil {
return fmt.Errorf("failed to marshal session for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "session.json"), sessionBytes, opts); err != nil {
return err
}
colorsSource := state.effectiveColorsSource(homeDir)
if err := copyFileWithPrivesc(colorsSource, filepath.Join(userDir, "colors.json"), opts); err != nil {
return fmt.Errorf("failed to copy colors for user cache slot: %w", err)
}
if err := syncUserProfileImage(homeDir, userDir, opts); err != nil {
return err
}
rootOverride := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
userOverride := filepath.Join(userDir, "greeter_wallpaper_override.jpg")
if st, statErr := os.Stat(rootOverride); statErr == nil && !st.IsDir() {
if err := copyFileWithPrivesc(rootOverride, userOverride, opts); err != nil {
return fmt.Errorf("failed to copy greeter wallpaper override for user cache slot: %w", err)
}
} else if opts.useDirectWrite(userOverride) {
_ = os.Remove(userOverride)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", userOverride)
}
logFunc(fmt.Sprintf("✓ Synced per-user greeter cache for %s", username))
return nil
}
func localizeSessionWallpapers(session map[string]any, userDir string, opts userSlotSyncOpts) error {
stringKeys := []struct {
key string
prefix string
}{
{"wallpaperPath", "wallpaper"},
{"wallpaperPathLight", "wallpaper-light"},
{"wallpaperPathDark", "wallpaper-dark"},
}
for _, item := range stringKeys {
if err := localizeWallpaperStringField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
mapKeys := []struct {
key string
prefix string
}{
{"monitorWallpapers", "wallpaper-monitor"},
{"monitorWallpapersLight", "wallpaper-monitor-light"},
{"monitorWallpapersDark", "wallpaper-monitor-dark"},
}
for _, item := range mapKeys {
if err := localizeWallpaperMapField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
return nil
}
func localizeWallpaperStringField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok {
return nil
}
path, ok := raw.(string)
if !ok || strings.TrimSpace(path) == "" {
return nil
}
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix, opts)
if err != nil {
return err
}
if dest != "" {
session[key] = dest
}
return nil
}
func localizeWallpaperMapField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok || raw == nil {
return nil
}
values, ok := raw.(map[string]any)
if !ok {
return nil
}
for monitor, rawPath := range values {
path, ok := rawPath.(string)
if !ok || strings.TrimSpace(path) == "" {
continue
}
safeMonitor := monitorWallpaperSanitizer.ReplaceAllString(monitor, "-")
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix+"-"+safeMonitor, opts)
if err != nil {
return err
}
if dest != "" {
values[monitor] = dest
}
}
return nil
}
func copyWallpaperIntoUserCache(srcPath, userDir, prefix string, opts userSlotSyncOpts) (string, error) {
if strings.TrimSpace(srcPath) == "" {
return "", nil
}
st, err := os.Stat(srcPath)
if err != nil || st.IsDir() {
return "", nil
}
ext := filepath.Ext(srcPath)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, prefix+ext)
if err := copyFileWithPrivesc(srcPath, dest, opts); err != nil {
return "", err
}
return dest, nil
}
func copyFileWithPrivesc(src, dest string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
if !isGreeterCachePath(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", dest)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", src, dest); err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", src, dest, err)
}
return setGreeterCacheFileOwnership(dest, opts.sudoPassword)
}
func writeFileWithPrivesc(path string, data []byte, opts userSlotSyncOpts) error {
if opts.useDirectWrite(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
if !isGreeterCachePath(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
tmp, err := os.CreateTemp("", "dms-greeter-user-cache-*")
if err != nil {
return fmt.Errorf("failed to create temp file for %s: %w", path, err)
}
tmpPath := tmp.Name()
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to write temp file for %s: %w", path, err)
}
if err := tmp.Close(); err != nil {
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to close temp file for %s: %w", path, err)
}
defer os.Remove(tmpPath)
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", tmpPath, path); err != nil {
return fmt.Errorf("failed to install %s: %w", path, err)
}
return setGreeterCacheFileOwnership(path, opts.sudoPassword)
}
func resolveUserProfileImageSource(homeDir string) string {
candidates := []string{
filepath.Join(homeDir, ".face"),
filepath.Join(homeDir, ".face.icon"),
}
if homeDir != "" {
username := filepath.Base(homeDir)
if username != "" && username != "." && username != string(filepath.Separator) {
candidates = append([]string{filepath.Join("/var/lib/AccountsService/icons", username)}, candidates...)
}
}
for _, src := range candidates {
st, err := os.Stat(src)
if err == nil && !st.IsDir() && st.Size() > 0 {
return src
}
}
return ""
}
func syncUserProfileImage(homeDir, userDir string, opts userSlotSyncOpts) error {
for _, name := range []string{"profile.jpg", "profile.jpeg", "profile.png", "profile.webp"} {
path := filepath.Join(userDir, name)
if opts.useDirectWrite(path) {
_ = os.Remove(path)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
}
}
src := resolveUserProfileImageSource(homeDir)
if src == "" {
return nil
}
ext := filepath.Ext(src)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, "profile"+ext)
if err := copyFileWithPrivesc(src, dest, opts); err != nil {
return fmt.Errorf("failed to copy profile image for user cache slot: %w", err)
}
return nil
}
@@ -1,81 +0,0 @@
package greeter
import (
"path/filepath"
"testing"
)
func TestUserGreeterCacheDir(t *testing.T) {
t.Parallel()
got := userGreeterCacheDir("/var/cache/dms-greeter", "alice")
want := filepath.Join("/var/cache/dms-greeter", "users", "alice")
if got != want {
t.Fatalf("userGreeterCacheDir() = %q, want %q", got, want)
}
}
func TestResolveUserProfileImageSource(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
facePath := filepath.Join(homeDir, ".face")
writeTestFile(t, facePath, "face")
got := resolveUserProfileImageSource(homeDir)
if got != facePath {
t.Fatalf("resolveUserProfileImageSource() = %q, want %q", got, facePath)
}
}
func TestIsUserOwnedGreeterCacheSlot(t *testing.T) {
t.Parallel()
slot := filepath.Join(GreeterCacheDir, "users", "alice", "settings.json")
if !isUserOwnedGreeterCacheSlot(slot, "alice") {
t.Fatalf("expected alice to own %q", slot)
}
if isUserOwnedGreeterCacheSlot(slot, "bob") {
t.Fatalf("expected bob not to own alice slot")
}
if isUserOwnedGreeterCacheSlot(filepath.Join(GreeterCacheDir, "settings.json"), "alice") {
t.Fatalf("expected root cache file not to be a user slot")
}
}
func TestLocalizeSessionWallpapers(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
userDir := filepath.Join(homeDir, "users", "alice")
wallpaperPath := filepath.Join(homeDir, "wall.jpg")
writeTestFile(t, wallpaperPath, "wallpaper")
session := map[string]any{
"wallpaperPath": wallpaperPath,
"monitorWallpapers": map[string]any{
"DP-1": wallpaperPath,
},
}
if err := localizeSessionWallpapers(session, userDir, userSlotSyncOpts{}); err != nil {
t.Fatalf("localizeSessionWallpapers returned error: %v", err)
}
gotPath, ok := session["wallpaperPath"].(string)
if !ok || gotPath == "" {
t.Fatalf("expected localized wallpaperPath, got %#v", session["wallpaperPath"])
}
if gotPath == wallpaperPath {
t.Fatalf("expected copied wallpaper path, still points to source")
}
monitorMap, ok := session["monitorWallpapers"].(map[string]any)
if !ok {
t.Fatalf("expected monitorWallpapers map")
}
monitorPath, ok := monitorMap["DP-1"].(string)
if !ok || monitorPath == "" || monitorPath == wallpaperPath {
t.Fatalf("expected localized monitor wallpaper, got %#v", monitorMap["DP-1"])
}
}
-1
View File
@@ -418,7 +418,6 @@ func handleConnection(conn net.Conn) {
conn.Write(capsData) conn.Write(capsData)
conn.Write([]byte("\n")) conn.Write([]byte("\n"))
scanner := bufio.NewScanner(conn) scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), 64*1024*1024) // grow up to 64 MB for large clipboard payloads
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
+4 -18
View File
@@ -1353,27 +1353,13 @@ Singleton {
} }
} }
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string greeterSessionBaseDir: root._greeterCacheDir
function setGreeterSessionBaseDir(dir) {
const next = dir || root._greeterCacheDir;
if (greeterSessionBaseDir === next)
return;
greeterSessionBaseDir = next;
if (isGreeterMode)
greeterSessionFile.reload();
}
function resetGreeterSessionBaseDir() {
setGreeterSessionBaseDir(root._greeterCacheDir);
}
FileView { FileView {
id: greeterSessionFile id: greeterSessionFile
path: root.greeterSessionBaseDir ? (root.greeterSessionBaseDir + "/session.json") : "" path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
return greetCfgDir + "/session.json";
}
preload: isGreeterMode preload: isGreeterMode
blockLoading: false blockLoading: false
blockWrites: true blockWrites: true
+3 -20
View File
@@ -2079,29 +2079,12 @@ Singleton {
} }
} }
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string greeterColorsBaseDir: root._greeterCacheDir
function setGreeterColorsBaseDir(dir) {
const next = dir || root._greeterCacheDir;
if (greeterColorsBaseDir === next)
return;
greeterColorsBaseDir = next;
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
dynamicColorsFileView.reload();
}
function resetGreeterColorsBaseDir() {
setGreeterColorsBaseDir(root._greeterCacheDir);
}
FileView { FileView {
id: dynamicColorsFileView id: dynamicColorsFileView
path: { path: {
if (SessionData.isGreeterMode) const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
return root.greeterColorsBaseDir ? (root.greeterColorsBaseDir + "/colors.json") : ""; const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
return stateDir + "/dms-colors.json"; return colorsPath;
} }
blockLoading: false blockLoading: false
watchChanges: !SessionData.isGreeterMode watchChanges: !SessionData.isGreeterMode
@@ -65,6 +65,15 @@ Item {
forceActiveFocus(); forceActiveFocus();
}); });
} }
Connections {
target: modal
function onOpened() {
Qt.callLater(function () {
searchField.forceActiveFocus();
});
}
}
} }
} }
+22 -29
View File
@@ -29,29 +29,32 @@ Item {
} }
try { try {
const decoded = Qt.atob(sanitized); const chars = new Array(sanitized.length);
if (!decoded) { for (let i = 0; i < sanitized.length; i++) {
return data; chars[i] = sanitized.charAt(i);
} }
let binary = ""; let buffer = null;
if (typeof decoded === "string") { if (typeof Qt !== "undefined" && typeof Qt.atob === "function") {
// Pre-6.11 Qt.atob returns a binary string directly buffer = Qt.atob(chars);
binary = decoded; } else if (typeof atob === "function") {
} else { const binary = atob(sanitized);
// Qt 6.11+ Qt.atob returns an ArrayBuffer convert to avoid O(n²) concat/stack limits const bytes = new Uint8Array(binary.length);
const bytes = new Uint8Array(decoded); for (let i = 0; i < binary.length; i++) {
const chunkSize = 8192; bytes[i] = binary.charCodeAt(i);
const chunks = [];
for (let i = 0; i < bytes.length; i += chunkSize) {
chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)));
} }
binary = chunks.join(""); buffer = bytes.buffer;
} }
if (!buffer || buffer.byteLength === 0) {
if (!binary) {
return data; return data;
} }
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
try { try {
return decodeURIComponent(escape(binary)); return decodeURIComponent(escape(binary));
} catch (e) { } catch (e) {
@@ -71,7 +74,6 @@ Item {
Qt.callLater(function () { Qt.callLater(function () {
if (editField) { if (editField) {
editField.forceActiveFocus(); editField.forceActiveFocus();
editField.cursorPosition = editField.text.length;
} }
}); });
@@ -102,17 +104,7 @@ Item {
} }
root.editorText = fullText; root.editorText = fullText;
if (editField) { if (editField) {
if (fullText.length > 50000) { editField.text = fullText;
Qt.callLater(function () {
if (editField) {
editField.text = fullText;
editField.cursorPosition = fullText.length;
}
});
} else {
editField.text = fullText;
editField.cursorPosition = fullText.length;
}
} }
}); });
} }
@@ -260,6 +252,7 @@ Item {
id: editField id: editField
width: editScroll.width width: editScroll.width
height: Math.max(editScroll.height, contentHeight) height: Math.max(editScroll.height, contentHeight)
text: root.editorText
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
wrapMode: TextEdit.Wrap wrapMode: TextEdit.Wrap
@@ -78,9 +78,10 @@ Rectangle {
onClicked: { onClicked: {
if (entryType === "image") { if (entryType === "image") {
return; // TODO - forward to editing software
} else {
editRequested();
} }
editRequested();
} }
} }
@@ -1,210 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
FocusScope {
id: root
property var clearConfirmDialog: null
property string activeTab: "recents"
property bool showKeyboardHints: false
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
property string mode: "history"
property string searchText: ClipboardService.searchText
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
readonly property int totalCount: ClipboardService.totalCount
readonly property var clipboardEntries: ClipboardService.clipboardEntries
readonly property var pinnedEntries: ClipboardService.pinnedEntries
readonly property int pinnedCount: ClipboardService.pinnedCount
readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
readonly property int selectedIndex: ClipboardService.selectedIndex
readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
readonly property var modalFocusScope: root
property alias searchField: historyContent.searchField
property alias editorView: editorView
property alias keyboardController: keyboardController
signal closeRequested
signal instantCloseRequested
onActiveTabChanged: {
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
}
onSearchTextChanged: ClipboardService.searchText = searchText
function hide() {
closeRequested();
}
function pasteSelected() {
ClipboardService.pasteSelected(() => root.instantCloseRequested());
}
function copyEntry(entry) {
ClipboardService.copyEntry(entry, () => root.closeRequested());
}
function deleteEntry(entry) {
ClipboardService.deleteEntry(entry);
}
function deletePinnedEntry(entry) {
ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
}
function pinEntry(entry) {
ClipboardService.pinEntry(entry);
}
function unpinEntry(entry) {
ClipboardService.unpinEntry(entry);
}
function clearAll() {
ClipboardService.clearAll();
}
function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry);
}
function getEntryType(entry) {
return ClipboardService.getEntryType(entry);
}
function updateFilteredModel() {
ClipboardService.updateFilteredModel();
}
function refreshClipboard() {
ClipboardService.refresh();
}
function editEntry(entry) {
if (!entry || entry.isImage) {
return;
}
editorView.setEntry(entry);
mode = "editor";
}
function resetState() {
activeImageLoads = 0;
mode = "history";
ClipboardService.reset();
keyboardController.reset();
}
focus: true
Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
ClipboardKeyboardController {
id: keyboardController
modal: root
}
Item {
id: historyView
anchors.fill: parent
opacity: 1
scale: 1
visible: opacity > 0.01
enabled: root.mode === "history"
ClipboardContent {
id: historyContent
anchors.fill: parent
modal: root
clearConfirmDialog: root.clearConfirmDialog
}
}
ClipboardEditor {
id: editorView
anchors.fill: parent
opacity: 0
scale: 0.98
visible: opacity > 0.01
enabled: root.mode === "editor"
focus: root.mode === "editor"
modal: root
keyController: keyboardController
}
states: [
State {
name: "history"
when: root.mode === "history"
PropertyChanges {
target: historyView
opacity: 1
scale: 1
}
PropertyChanges {
target: editorView
opacity: 0
scale: 0.98
}
},
State {
name: "editor"
when: root.mode === "editor"
PropertyChanges {
target: historyView
opacity: 0
scale: 0.98
}
PropertyChanges {
target: editorView
opacity: 1
scale: 1
}
}
]
transitions: [
Transition {
from: "history"
to: "editor"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
},
Transition {
from: "editor"
to: "history"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
}
@@ -17,28 +17,74 @@ DankModal {
active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
} }
property string activeTab: "recents"
onActiveTabChanged: {
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
}
property bool showKeyboardHints: false
property Component clipboardContent
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
readonly property int totalCount: ClipboardService.totalCount
readonly property var clipboardEntries: ClipboardService.clipboardEntries
readonly property var pinnedEntries: ClipboardService.pinnedEntries
readonly property int pinnedCount: ClipboardService.pinnedCount
readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
readonly property int selectedIndex: ClipboardService.selectedIndex
readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
property string searchText: ClipboardService.searchText
onSearchTextChanged: ClipboardService.searchText = searchText
Ref {
service: ClipboardService
}
property string mode: "history"
onModeChanged: {
if (mode !== "history") {
return;
}
Qt.callLater(function () {
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.forceActiveFocus();
}
});
}
function updateFilteredModel() {
ClipboardService.updateFilteredModel();
}
function pasteSelected() {
ClipboardService.pasteSelected(instantClose);
}
function toggle() { function toggle() {
if (shouldBeVisible) { if (shouldBeVisible) {
hide(); hide();
return; } else {
show();
} }
show();
} }
function show() { function show() {
open(); open();
mode = "history";
activeImageLoads = 0;
shouldHaveFocus = true; shouldHaveFocus = true;
ClipboardService.reset();
keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (contentLoader.item) { if (clipboardAvailable) {
contentLoader.item.resetState();
}
if (clipboardHistoryModal.clipboardAvailable) {
if (Theme.isConnectedEffect) { if (Theme.isConnectedEffect) {
Qt.callLater(() => { Qt.callLater(() => {
if (clipboardHistoryModal.shouldBeVisible) { if (clipboardHistoryModal.shouldBeVisible)
ClipboardService.refresh(); ClipboardService.refresh();
}
}); });
} else { } else {
ClipboardService.refresh(); ClipboardService.refresh();
@@ -56,12 +102,61 @@ DankModal {
} }
onDialogClosed: { onDialogClosed: {
if (contentLoader.item) { activeImageLoads = 0;
contentLoader.item.resetState(); ClipboardService.reset();
} keyboardController.reset();
} }
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable function refreshClipboard() {
ClipboardService.refresh();
}
function copyEntry(entry) {
ClipboardService.copyEntry(entry, hide);
}
function deleteEntry(entry) {
ClipboardService.deleteEntry(entry);
}
function deletePinnedEntry(entry) {
ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
}
function pinEntry(entry) {
ClipboardService.pinEntry(entry);
}
function unpinEntry(entry) {
ClipboardService.unpinEntry(entry);
}
function clearAll() {
ClipboardService.clearAll();
}
function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry);
}
function getEntryType(entry) {
return ClipboardService.getEntryType(entry);
}
function editEntry(entry) {
if (!entry) {
return;
}
if (entry.isImage) {
return;
}
const editor = contentLoader.item?.editorView;
if (!editor) {
return;
}
editor.setEntry(entry);
mode = "editor";
}
visible: false visible: false
modalWidth: ClipboardConstants.modalWidth modalWidth: ClipboardConstants.modalWidth
@@ -71,11 +166,16 @@ DankModal {
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
borderWidth: 1 borderWidth: 1
enableShadow: true enableShadow: true
closeOnEscapeKey: (contentLoader.item?.mode ?? "history") !== "editor" closeOnEscapeKey: mode !== "editor"
onBackgroundClicked: hide() onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
content: clipboardContent
Ref { ClipboardKeyboardController {
service: ClipboardService id: keyboardController
modal: clipboardHistoryModal
} }
ConfirmModal { ConfirmModal {
@@ -100,11 +200,112 @@ DankModal {
} }
} }
content: Component { property var confirmDialog: clearConfirmDialog
ClipboardHistoryContent {
clearConfirmDialog: clearConfirmDialog clipboardContent: Component {
onCloseRequested: clipboardHistoryModal.hide() Item {
onInstantCloseRequested: clipboardHistoryModal.instantClose() id: viewContainer
property alias editorView: editorView
property alias searchField: historyContent.searchField
anchors.fill: parent
Item {
id: historyView
anchors.fill: parent
opacity: 1
scale: 1
visible: opacity > 0.01
enabled: clipboardHistoryModal.mode === "history"
ClipboardContent {
id: historyContent
anchors.fill: parent
modal: clipboardHistoryModal
clearConfirmDialog: clipboardHistoryModal.confirmDialog
}
}
ClipboardEditor {
id: editorView
anchors.fill: parent
opacity: 0
scale: 0.98
visible: opacity > 0.01
enabled: clipboardHistoryModal.mode === "editor"
focus: clipboardHistoryModal.mode === "editor"
modal: clipboardHistoryModal
keyController: keyboardController
}
states: [
State {
name: "history"
when: clipboardHistoryModal.mode === "history"
PropertyChanges {
target: historyView
opacity: 1
scale: 1
}
PropertyChanges {
target: editorView
opacity: 0
scale: 0.98
}
},
State {
name: "editor"
when: clipboardHistoryModal.mode === "editor"
PropertyChanges {
target: historyView
opacity: 0
scale: 0.98
}
PropertyChanges {
target: editorView
opacity: 1
scale: 1
}
}
]
transitions: [
Transition {
from: "history"
to: "editor"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
},
Transition {
from: "editor"
to: "history"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
} }
} }
} }
@@ -15,20 +15,47 @@ DankPopout {
property var parentWidget: null property var parentWidget: null
property var triggerScreen: null property var triggerScreen: null
property string activeTab: "recents" property string activeTab: "recents"
property bool showKeyboardHints: false
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
readonly property int totalCount: ClipboardService.totalCount
readonly property var clipboardEntries: ClipboardService.clipboardEntries
readonly property var pinnedEntries: ClipboardService.pinnedEntries
readonly property int pinnedCount: ClipboardService.pinnedCount readonly property int pinnedCount: ClipboardService.pinnedCount
readonly property var confirmDialog: clearConfirmDialog readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
readonly property int selectedIndex: ClipboardService.selectedIndex
readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
property string searchText: ClipboardService.searchText
onSearchTextChanged: ClipboardService.searchText = searchText
readonly property var modalFocusScope: contentLoader.item ?? null readonly property var modalFocusScope: contentLoader.item ?? null
Ref {
service: ClipboardService
}
function updateFilteredModel() {
ClipboardService.updateFilteredModel();
}
function pasteSelected() {
ClipboardService.pasteSelected(instantClose);
}
function instantClose() {
close();
}
function show() { function show() {
open(); open();
activeImageLoads = 0;
ClipboardService.reset();
keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (contentLoader.item) {
contentLoader.item.activeTab = activeTab;
contentLoader.item.resetState();
}
if (contentLoader.item?.searchField) { if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = ""; contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus();
@@ -38,12 +65,47 @@ DankPopout {
function hide() { function hide() {
close(); close();
activeImageLoads = 0;
ClipboardService.reset();
keyboardController.reset();
}
function refreshClipboard() {
ClipboardService.refresh();
}
function copyEntry(entry) {
ClipboardService.copyEntry(entry, hide);
}
function deleteEntry(entry) {
ClipboardService.deleteEntry(entry);
}
function deletePinnedEntry(entry) {
ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
}
function pinEntry(entry) {
ClipboardService.pinEntry(entry);
}
function unpinEntry(entry) {
ClipboardService.unpinEntry(entry);
} }
function clearAll() { function clearAll() {
ClipboardService.clearAll(); ClipboardService.clearAll();
} }
function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry);
}
function getEntryType(entry) {
return ClipboardService.getEntryType(entry);
}
popupWidth: ClipboardConstants.popoutWidth popupWidth: ClipboardConstants.popoutWidth
popupHeight: ClipboardConstants.popoutHeight popupHeight: ClipboardConstants.popoutHeight
triggerWidth: 55 triggerWidth: 55
@@ -55,25 +117,20 @@ DankPopout {
onBackgroundClicked: hide() onBackgroundClicked: hide()
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (!shouldBeVisible) { if (!shouldBeVisible)
return; return;
}
if (clipboardAvailable) { if (clipboardAvailable) {
if (Theme.isConnectedEffect) { if (Theme.isConnectedEffect) {
Qt.callLater(() => { Qt.callLater(() => {
if (root.shouldBeVisible) { if (root.shouldBeVisible)
ClipboardService.refresh(); ClipboardService.refresh();
}
}); });
} else { } else {
ClipboardService.refresh(); ClipboardService.refresh();
} }
} }
keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (contentLoader.item) {
contentLoader.item.activeTab = activeTab;
contentLoader.item.resetState();
}
if (contentLoader.item?.searchField) { if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = ""; contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus();
@@ -82,13 +139,14 @@ DankPopout {
} }
onPopoutClosed: { onPopoutClosed: {
if (contentLoader.item) { activeImageLoads = 0;
contentLoader.item.resetState(); ClipboardService.reset();
} keyboardController.reset();
} }
Ref { ClipboardKeyboardController {
service: ClipboardService id: keyboardController
modal: root
} }
ConfirmModal { ConfirmModal {
@@ -97,20 +155,48 @@ DankPopout {
confirmButtonColor: Theme.primary confirmButtonColor: Theme.primary
} }
property var confirmDialog: clearConfirmDialog
content: Component { content: Component {
ClipboardHistoryContent { FocusScope {
id: contentFocusScope
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
clearConfirmDialog: clearConfirmDialog focus: true
onCloseRequested: root.hide()
onInstantCloseRequested: root.close() property alias searchField: clipboardContentItem.searchField
Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
Component.onCompleted: { Component.onCompleted: {
activeTab = root.activeTab; if (root.shouldBeVisible)
if (root.shouldBeVisible) {
forceActiveFocus(); forceActiveFocus();
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(() => contentFocusScope.forceActiveFocus());
}
} }
function onOpened() {
Qt.callLater(() => {
if (clipboardContentItem.searchField) {
clipboardContentItem.searchField.forceActiveFocus();
}
});
}
}
ClipboardContent {
id: clipboardContentItem
modal: root
clearConfirmDialog: root.confirmDialog
} }
} }
} }
+8 -16
View File
@@ -25,14 +25,14 @@ DankPopout {
property int __dropdownType: 0 property int __dropdownType: 0
property point __dropdownAnchor: Qt.point(0, 0) property point __dropdownAnchor: Qt.point(0, 0)
property bool __dropdownRightEdge: false property bool __dropdownRightEdge: false
property var __dropdownPlayer: MprisController.activePlayer property var __dropdownPlayer: null
property var __dropdownPlayers: MprisController.availablePlayers property var __dropdownPlayers: []
function __showVolumeDropdown(pos, rightEdge, player, players) { function __showVolumeDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos; __dropdownAnchor = pos;
__dropdownRightEdge = rightEdge; __dropdownRightEdge = rightEdge;
__dropdownPlayer = Qt.binding(() => MprisController.activePlayer); __dropdownPlayer = player;
__dropdownPlayers = Qt.binding(() => MprisController.availablePlayers); __dropdownPlayers = players;
__dropdownType = 1; __dropdownType = 1;
} }
@@ -45,8 +45,8 @@ DankPopout {
function __showPlayersDropdown(pos, rightEdge, player, players) { function __showPlayersDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos; __dropdownAnchor = pos;
__dropdownRightEdge = rightEdge; __dropdownRightEdge = rightEdge;
__dropdownPlayer = Qt.binding(() => MprisController.activePlayer); __dropdownPlayer = player;
__dropdownPlayers = Qt.binding(() => MprisController.availablePlayers); __dropdownPlayers = players;
__dropdownType = 3; __dropdownType = 3;
} }
@@ -69,7 +69,7 @@ DankPopout {
id: __volumeCloseTimer id: __volumeCloseTimer
interval: 400 interval: 400
onTriggered: { onTriggered: {
if (__dropdownType !== 0) { if (__dropdownType === 1) {
__hideDropdowns(); __hideDropdowns();
} }
} }
@@ -230,13 +230,6 @@ DankPopout {
return; return;
} }
if (root.currentTabIndex === 1 && mediaLoader.item?.handleKeyEvent) {
if (mediaLoader.item.handleKeyEvent(event)) {
event.accepted = true;
return;
}
}
if (root.currentTabIndex === 2 && wallpaperLoader.item?.handleKeyEvent) { if (root.currentTabIndex === 2 && wallpaperLoader.item?.handleKeyEvent) {
if (wallpaperLoader.item.handleKeyEvent(event)) { if (wallpaperLoader.item.handleKeyEvent(event)) {
event.accepted = true; event.accepted = true;
@@ -401,8 +394,7 @@ DankPopout {
root.__showPlayersDropdown(pos, rightEdge, player, players); root.__showPlayersDropdown(pos, rightEdge, player, players);
} }
onHideDropdowns: root.__hideDropdowns() onHideDropdowns: root.__hideDropdowns()
onDropdownButtonExited: root.__startCloseTimer() onVolumeButtonExited: root.__startCloseTimer()
onDropdownButtonEntered: root.__stopCloseTimer()
} }
} }
} }
@@ -42,22 +42,16 @@ Item {
signal panelEntered signal panelEntered
signal panelExited signal panelExited
property int __panelHoverCount: 0 property int __volumeHoverCount: 0
onDropdownTypeChanged: { function volumeAreaEntered() {
if (dropdownType === 0) { __volumeHoverCount++;
__panelHoverCount = 0;
}
}
function panelAreaEntered() {
__panelHoverCount++;
panelEntered(); panelEntered();
} }
function panelAreaExited() { function volumeAreaExited() {
__panelHoverCount = Math.max(0, __panelHoverCount - 1); __volumeHoverCount = Math.max(0, __volumeHoverCount - 1);
if (__panelHoverCount === 0) if (__volumeHoverCount === 0)
panelExited(); panelExited();
} }
@@ -137,8 +131,8 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: -12 anchors.margins: -12
hoverEnabled: true hoverEnabled: true
onEntered: panelAreaEntered() onEntered: volumeAreaEntered()
onExited: panelAreaExited() onExited: volumeAreaExited()
} }
Item { Item {
@@ -196,8 +190,8 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
preventStealing: true preventStealing: true
onEntered: panelAreaEntered() onEntered: volumeAreaEntered()
onExited: panelAreaExited() onExited: volumeAreaExited()
onPressed: mouse => updateVolume(mouse) onPressed: mouse => updateVolume(mouse)
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (pressed) if (pressed)
@@ -275,14 +269,6 @@ Item {
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
} }
MouseArea {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
}
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
@@ -363,13 +349,7 @@ Item {
} }
StyledText { StyledText {
text: { text: modelData === AudioService.sink ? "Active" : "Available"
if (!modelData?.audio)
return modelData === AudioService.sink ? I18n.tr("Active") : I18n.tr("Available");
if (modelData.audio.muted)
return I18n.tr("Muted", "audio status");
return Math.round(modelData.audio.volume * 100) + "%";
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
@@ -389,8 +369,6 @@ Item {
root.deviceSelected(modelData); root.deviceSelected(modelData);
} }
} }
onEntered: panelAreaEntered()
onExited: panelAreaExited()
} }
} }
} }
@@ -447,14 +425,6 @@ Item {
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
} }
MouseArea {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
}
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
@@ -528,7 +498,15 @@ Item {
} }
StyledText { StyledText {
text: modelData?.trackArtist || I18n.tr("Unknown Artist") text: {
if (!modelData)
return "";
const artist = modelData.trackArtist || "";
const isActive = modelData === activePlayer;
if (artist.length > 0)
return artist + (isActive ? " (Active)" : "");
return isActive ? "Active" : "Available";
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
@@ -548,8 +526,6 @@ Item {
root.playerSelected(modelData); root.playerSelected(modelData);
} }
} }
onEntered: panelAreaEntered()
onExited: panelAreaExited()
} }
} }
} }
+44 -171
View File
@@ -13,7 +13,6 @@ Item {
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
readonly property real stableLength: MprisController.activePlayerStableLength
property var allPlayers: MprisController.availablePlayers property var allPlayers: MprisController.availablePlayers
property var targetScreen: null property var targetScreen: null
property real popoutX: 0 property real popoutX: 0
@@ -28,8 +27,7 @@ Item {
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge) signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players) signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal hideDropdowns signal hideDropdowns
signal dropdownButtonExited signal volumeButtonExited
signal dropdownButtonEntered
property bool volumeExpanded: false property bool volumeExpanded: false
property bool devicesExpanded: false property bool devicesExpanded: false
@@ -41,7 +39,9 @@ Item {
playersExpanded = false; playersExpanded = false;
} }
DankTooltipV2 {
id: sharedTooltip
}
readonly property bool isRightEdge: { readonly property bool isRightEdge: {
if (barPosition === SettingsData.Position.Right) if (barPosition === SettingsData.Position.Right)
@@ -65,7 +65,8 @@ Item {
// Derived "no players" state: always correct, no timers. // Derived "no players" state: always correct, no timers.
readonly property int _playerCount: allPlayers ? allPlayers.length : 0 readonly property int _playerCount: allPlayers ? allPlayers.length : 0
readonly property bool _noneAvailable: _playerCount === 0 readonly property bool _noneAvailable: _playerCount === 0
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || !activePlayer) readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
property bool _switchHold: false property bool _switchHold: false
Timer { Timer {
@@ -84,6 +85,7 @@ Item {
isSwitching = true; isSwitching = true;
_switchHold = true; _switchHold = true;
_switchHoldTimer.restart(); _switchHoldTimer.restart();
TrackArtService.loadArtwork(activePlayer.trackArtUrl);
} }
function maybeFinishSwitch() { function maybeFinishSwitch() {
@@ -94,11 +96,11 @@ Item {
} }
readonly property real ratio: { readonly property real ratio: {
if (!activePlayer || stableLength <= 0) { if (!activePlayer || !activePlayer.length || activePlayer.length <= 0) {
return 0; return 0;
} }
const pos = (activePlayer.position || 0) % Math.max(1, stableLength); const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
const calculatedRatio = pos / stableLength; const calculatedRatio = pos / activePlayer.length;
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
@@ -107,11 +109,13 @@ Item {
Connections { Connections {
target: activePlayer target: activePlayer
ignoreUnknownSignals: true
function onTrackTitleChanged() { function onTrackTitleChanged() {
_switchHoldTimer.restart(); _switchHoldTimer.restart();
maybeFinishSwitch(); maybeFinishSwitch();
} }
function onTrackArtUrlChanged() {
TrackArtService.loadArtwork(activePlayer.trackArtUrl);
}
} }
Connections { Connections {
@@ -182,102 +186,6 @@ Item {
} }
} }
function triggerVolumeDropdown() {
if (!volumeAvailable)
return;
if (volumeExpanded)
return;
hideDropdowns();
volumeExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = volumeButton.y + volumeButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
function toggleMute() {
if (!volumeAvailable)
return;
SessionData.suppressOSDTemporarily();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = restoreVolume;
}
}
}
function handleKeyEvent(event) {
if (!activePlayer)
return false;
// 1. Number keys 0-9 to seek to 0%-90%
if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
if (activePlayer.canSeek && stableLength > 0) {
const ratio = (event.key - Qt.Key_0) * 0.1;
const targetPosition = ratio * stableLength;
activePlayer.position = Math.max(0.1, Math.min(targetPosition, stableLength * 0.99));
return true;
}
}
// 2. Left / Right arrows to seek backward / forward 5s
if (event.key === Qt.Key_Left) {
if (activePlayer.canSeek) {
activePlayer.position = Math.max(0.1, activePlayer.position - 5);
return true;
}
}
if (event.key === Qt.Key_Right) {
if (activePlayer.canSeek && stableLength > 0) {
activePlayer.position = Math.max(0.1, Math.min(stableLength - 1, activePlayer.position + 5));
return true;
}
}
// 3. Up / Down arrows to adjust volume
if (event.key === Qt.Key_Up) {
adjustVolume(5);
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
if (event.key === Qt.Key_Down) {
adjustVolume(-5);
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
// 4. Spacebar to play/pause
if (event.key === Qt.Key_Space) {
if (activePlayer.canTogglePlaying) {
activePlayer.togglePlaying();
return true;
}
}
// 5. M key to toggle mute
if (event.key === Qt.Key_M) {
toggleMute();
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
return false;
}
property bool isSeeking: false property bool isSeeking: false
Timer { Timer {
@@ -290,14 +198,14 @@ Item {
Item { Item {
id: bgContainer id: bgContainer
anchors.fill: parent anchors.fill: parent
visible: TrackArtService.resolvedArtUrl !== "" visible: TrackArtService._bgArtSource !== ""
Image { Image {
id: bgImage id: bgImage
anchors.centerIn: parent anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1 width: Math.max(parent.width, parent.height) * 1.1
height: width height: width
source: TrackArtService.resolvedArtUrl source: TrackArtService._bgArtSource
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: true asynchronous: true
cache: true cache: true
@@ -423,7 +331,7 @@ Item {
} }
StyledText { StyledText {
text: activePlayer?.trackArtist || I18n.tr("Unknown Artist") text: activePlayer?.trackTitle || I18n.tr("Unknown Artist")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width width: parent.width
@@ -481,7 +389,7 @@ Item {
if (!activePlayer) if (!activePlayer)
return "0:00"; return "0:00";
const rawPos = Math.max(0, activePlayer.position || 0); const rawPos = Math.max(0, activePlayer.position || 0);
const pos = stableLength ? rawPos % Math.max(1, stableLength) : rawPos; const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
const minutes = Math.floor(pos / 60); const minutes = Math.floor(pos / 60);
const seconds = Math.floor(pos % 60); const seconds = Math.floor(pos % 60);
const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds; const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
@@ -495,9 +403,9 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
if (!activePlayer || stableLength <= 0) if (!activePlayer || !activePlayer.length)
return "--:--"; return "0:00";
const dur = stableLength; const dur = Math.max(0, activePlayer.length || 0);
const minutes = Math.floor(dur / 60); const minutes = Math.floor(dur / 60);
const seconds = Math.floor(dur % 60); const seconds = Math.floor(dur % 60);
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds; return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
@@ -739,17 +647,7 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (playersExpanded) { if (playersExpanded) {
if (allPlayers && allPlayers.length > 1) { hideDropdowns();
let currentIndex = -1;
for (let i = 0; i < allPlayers.length; i++) {
if (allPlayers[i] === activePlayer) {
currentIndex = i;
break;
}
}
const nextIndex = (currentIndex + 1) % allPlayers.length;
MprisController.setActivePlayer(allPlayers[nextIndex]);
}
return; return;
} }
hideDropdowns(); hideDropdowns();
@@ -760,22 +658,8 @@ Item {
const screenY = popoutY + contentOffsetY + btnY; const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers); showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
} }
onEntered: { onEntered: sharedTooltip.show(I18n.tr("Media Players"), playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
dropdownButtonEntered(); onExited: sharedTooltip.hide()
if (playersExpanded)
return;
hideDropdowns();
playersExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = playerSelectorButton.y + playerSelectorButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onExited: {
if (playersExpanded)
dropdownButtonExited();
}
} }
} }
@@ -807,7 +691,6 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onEntered: { onEntered: {
dropdownButtonEntered();
if (volumeExpanded) if (volumeExpanded)
return; return;
hideDropdowns(); hideDropdowns();
@@ -820,10 +703,25 @@ Item {
} }
onExited: { onExited: {
if (volumeExpanded) if (volumeExpanded)
dropdownButtonExited(); volumeButtonExited();
} }
onClicked: { onClicked: {
toggleMute(); SessionData.suppressOSDTemporarily();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = restoreVolume;
}
}
} }
onWheel: wheelEvent => { onWheel: wheelEvent => {
SessionData.suppressOSDTemporarily(); SessionData.suppressOSDTemporarily();
@@ -856,7 +754,7 @@ Item {
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "speaker" name: devicesExpanded ? "expand_less" : "speaker"
size: 18 size: 18
color: Theme.surfaceText color: Theme.surfaceText
} }
@@ -868,18 +766,7 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (devicesExpanded) { if (devicesExpanded) {
const sinks = AudioService.getAvailableSinks(); hideDropdowns();
if (sinks && sinks.length > 1) {
let currentIndex = -1;
for (let i = 0; i < sinks.length; i++) {
if (sinks[i]?.name === AudioService.sink?.name) {
currentIndex = i;
break;
}
}
const nextIndex = (currentIndex + 1) % sinks.length;
AudioService.setSink(sinks[nextIndex]);
}
return; return;
} }
hideDropdowns(); hideDropdowns();
@@ -890,22 +777,8 @@ Item {
const screenY = popoutY + contentOffsetY + btnY; const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight); showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
} }
onEntered: { onEntered: sharedTooltip.show(I18n.tr("Output Device"), audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
dropdownButtonEntered(); onExited: sharedTooltip.hide()
if (devicesExpanded)
return;
hideDropdowns();
devicesExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = audioDevicesButton.y + audioDevicesButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
}
onExited: {
if (devicesExpanded)
dropdownButtonExited();
}
} }
} }
} }
@@ -15,11 +15,10 @@ Card {
property real displayPosition: currentPosition property real displayPosition: currentPosition
readonly property real ratio: { readonly property real ratio: {
const len = MprisController.activePlayerStableLength; if (!activePlayer || activePlayer.length <= 0)
if (!activePlayer || !activePlayer.lengthSupported || len <= 0)
return 0; return 0;
const pos = displayPosition % Math.max(1, len); const pos = displayPosition % Math.max(1, activePlayer.length);
const calculatedRatio = pos / len; const calculatedRatio = pos / activePlayer.length;
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
+7 -15
View File
@@ -12,24 +12,16 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("GreetdSettings") readonly property var log: Log.scoped("GreetdSettings")
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter" readonly property string configPath: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
property string configBaseDir: root._greeterCacheDir return greetCfgDir + "/settings.json";
readonly property string configPath: root.configBaseDir ? (root.configBaseDir + "/settings.json") : ""
readonly property string greeterWallpaperOverridePath: root.configBaseDir ? (root.configBaseDir + "/greeter_wallpaper_override.jpg") : ""
function setConfigBaseDir(dir) {
const next = dir || root._greeterCacheDir;
if (configBaseDir === next)
return;
configBaseDir = next;
settingsLoaded = false;
settingsFile.reload();
} }
function resetConfigBaseDir() { readonly property string _greeterCacheDir: {
setConfigBaseDir(root._greeterCacheDir); const i = root.configPath.lastIndexOf("/");
return i >= 0 ? root.configPath.substring(0, i) : "";
} }
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
property string currentThemeName: "purple" property string currentThemeName: "purple"
property bool settingsLoaded: false property bool settingsLoaded: false
+59 -270
View File
@@ -62,14 +62,6 @@ Item {
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f") readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f) readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
readonly property bool multipleUsersAvailable: GreeterUsersService.loaded && GreeterUsersService.users.length > 1
readonly property bool showUserPicker: multipleUsersAvailable && !GreeterState.showPasswordInput && !manualUsernameEntry
readonly property bool showAccountSwitchLink: multipleUsersAvailable && !GreeterState.showPasswordInput && !GreeterState.unlocking
readonly property int userPickerMaxHeight: Math.min(400, Math.max(120, height * 0.35))
property bool userListOpen: false
property bool manualUsernameEntry: false
property bool skipAutoSelectUser: false
property string pickerThemeUsername: ""
function initWeatherService() { function initWeatherService() {
if (weatherInitialized) if (weatherInitialized)
@@ -436,87 +428,20 @@ Item {
fprintdDeviceProbe.running = true; fprintdDeviceProbe.running = true;
} }
function applyPickerPreviewTheme() {
let previewUser = (pickerThemeUsername || "").trim();
if (!previewUser && GreetdSettings.rememberLastUser)
previewUser = (GreetdMemory.lastSuccessfulUser || "").trim();
if (previewUser)
GreeterUserTheme.applyForUser(previewUser);
else
GreeterUserTheme.applyDefault();
}
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
if (root.skipAutoSelectUser)
return;
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser) if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return; return;
const lastUser = GreetdMemory.lastSuccessfulUser; const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
selectUser(lastUser, true); GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser);
maybeAutoStartExternalAuth();
} }
} }
function enterManualUsernameEntry() { function submitUsername(rawValue) {
if (!root.multipleUsersAvailable || GreeterState.showPasswordInput)
return;
root.manualUsernameEntry = true;
root.userListOpen = false;
GreeterState.username = "";
GreeterState.usernameInput = "";
GreeterState.selectedUserIndex = -1;
inputField.text = "";
root.applyPickerPreviewTheme();
Qt.callLater(() => inputField.forceActiveFocus());
}
function returnToUserListFromManualEntry() {
if (!root.multipleUsersAvailable)
return;
root.manualUsernameEntry = false;
root.userListOpen = true;
GreeterState.username = "";
GreeterState.usernameInput = "";
inputField.text = "";
root.applyPickerPreviewTheme();
}
function returnToUserPicker() {
if (!root.multipleUsersAvailable || GreeterState.unlocking)
return;
root.manualUsernameEntry = false;
root.skipAutoSelectUser = true;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
passwordSubmitRequested = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
clearAuthFeedback();
passwordFailureCount = 0;
externalAuthAutoStartedForUser = "";
if (Greetd.state !== GreetdState.Inactive)
Greetd.cancelSession();
const previousUser = GreeterState.username;
GreeterState.reset();
inputField.text = "";
PortalService.profileImage = "";
if (previousUser)
root.pickerThemeUsername = previousUser;
root.applyPickerPreviewTheme();
root.userListOpen = true;
}
function selectUser(rawValue, skipDropdownUpdate) {
const user = (rawValue || "").trim();
if (!user)
return;
root.manualUsernameEntry = false;
root.skipAutoSelectUser = false;
submitUsername(user, skipDropdownUpdate === true);
}
function submitUsername(rawValue, skipDropdownUpdate) {
const user = (rawValue || "").trim(); const user = (rawValue || "").trim();
if (!user) if (!user)
return; return;
@@ -525,15 +450,8 @@ Item {
clearAuthFeedback(); clearAuthFeedback();
externalAuthAutoStartedForUser = ""; externalAuthAutoStartedForUser = "";
} }
root.pickerThemeUsername = user;
GreeterState.username = user; GreeterState.username = user;
GreeterState.usernameInput = user;
GreeterState.showPasswordInput = true; GreeterState.showPasswordInput = true;
if (!skipDropdownUpdate && typeof GreeterUsersService !== "undefined") {
const idx = GreeterUsersService.usernames.indexOf(user);
GreeterState.selectedUserIndex = idx;
}
root.userListOpen = false;
PortalService.getGreeterUserProfileImage(user); PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
pendingPasswordResponse = false; pendingPasswordResponse = false;
@@ -719,44 +637,13 @@ Item {
} }
} }
Connections {
target: GreeterUsersService
function onLoadedChanged() {
if (GreeterUsersService.loaded && isPrimaryScreen)
applyPickerPreviewTheme();
}
function onSyncedThemePathsChanged() {
if (!isPrimaryScreen)
return;
if (GreeterState.username)
GreeterUserTheme.applyForUser(GreeterState.username);
else if (root.showUserPicker || root.userListOpen)
applyPickerPreviewTheme();
}
}
Connections { Connections {
target: GreeterState target: GreeterState
function onUsernameChanged() { function onUsernameChanged() {
if (GreeterState.username) { if (GreeterState.username) {
root.pickerThemeUsername = GreeterState.username;
GreeterUserTheme.applyForUser(GreeterState.username);
PortalService.getGreeterUserProfileImage(GreeterState.username); PortalService.getGreeterUserProfileImage(GreeterState.username);
} else if (root.showUserPicker || root.userListOpen) {
applyPickerPreviewTheme();
} }
} }
function onShowPasswordInputChanged() {
if (GreeterState.showPasswordInput)
root.userListOpen = false;
}
}
onShowUserPickerChanged: {
if (showUserPicker && !GreeterState.username)
applyPickerPreviewTheme();
if (!showUserPicker)
userListOpen = false;
} }
FileView { FileView {
@@ -849,26 +736,19 @@ Item {
anchors.fill: parent anchors.fill: parent
color: "transparent" color: "transparent"
Column { Item {
id: greeterMainColumn id: clockContainer
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.bottom: parent.verticalCenter
spacing: Theme.spacingM anchors.bottomMargin: 60
width: 380 width: parent.width
height: clockText.implicitHeight
Item { Row {
id: clockContainer id: clockText
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width anchors.top: parent.top
height: clockText.implicitHeight spacing: 0
Row {
id: clockText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: 0
property string fullTimeStr: { property string fullTimeStr: {
const format = GreetdSettings.getEffectiveTimeFormat(); const format = GreetdSettings.getEffectiveTimeFormat();
@@ -973,121 +853,60 @@ Item {
visible: clockText.ampm !== "" visible: clockText.ampm !== ""
} }
} }
}
StyledText {
id: dateText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: clockContainer.bottom
anchors.topMargin: 4
text: {
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
} }
font.pixelSize: Theme.fontSizeXLarge
color: "white"
opacity: 0.9
}
StyledText { Item {
id: dateText anchors.horizontalCenter: parent.horizontalCenter
anchors.top: dateText.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Theme.spacingL
text: systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat()) width: 380
font.pixelSize: Theme.fontSizeXLarge height: 140
color: "white"
opacity: 0.9
}
StyledText {
id: userPickerHint
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showUserPicker && !GreeterState.showPasswordInput && !GreeterState.username && !root.userListOpen
text: I18n.tr("Select user...", "greeter user picker placeholder")
font.pixelSize: Theme.fontSizeMedium
color: "white"
opacity: 0.85
}
ColumnLayout { ColumnLayout {
id: authColumn anchors.fill: parent
width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
RowLayout { RowLayout {
spacing: Theme.spacingL spacing: Theme.spacingL
Layout.fillWidth: true Layout.fillWidth: true
Item { DankCircularImage {
Layout.preferredWidth: 60 Layout.preferredWidth: 60
Layout.preferredHeight: 60 Layout.preferredHeight: 60
visible: GreetdSettings.lockScreenShowProfileImage || root.multipleUsersAvailable imageSource: {
if (PortalService.profileImage === "")
DankCircularImage { return "";
anchors.fill: parent if (PortalService.profileImage.startsWith("/"))
imageSource: { return encodeFileUrl(PortalService.profileImage);
const displayUser = GreeterState.username || root.pickerThemeUsername; return PortalService.profileImage;
if (displayUser) {
const cachedPath = GreeterUsersService.profileImagePath(displayUser);
if (cachedPath)
return encodeFileUrl(cachedPath);
}
if (PortalService.profileImage === "")
return "";
if (PortalService.profileImage.startsWith("/"))
return encodeFileUrl(PortalService.profileImage);
return PortalService.profileImage;
}
fallbackIcon: "person"
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.color: Theme.primary
border.width: avatarPickerArea.containsMouse || root.userListOpen ? 2 : 0
visible: root.multipleUsersAvailable
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
id: avatarPickerArea
anchors.fill: parent
visible: root.multipleUsersAvailable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (GreeterState.showPasswordInput)
root.returnToUserPicker();
else if (root.manualUsernameEntry)
root.returnToUserListFromManualEntry();
else
root.userListOpen = !root.userListOpen;
}
} }
fallbackIcon: "person"
visible: GreetdSettings.lockScreenShowProfileImage
} }
Rectangle { Rectangle {
property bool showPassword: false property bool showPassword: false
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: root.showUserPicker && root.userListOpen ? Math.max(60, userPicker.implicitHeight + Theme.spacingM * 2) : 60 Layout.preferredHeight: 60
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3) border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
border.width: inputField.activeFocus ? 2 : 1 border.width: inputField.activeFocus ? 2 : 1
GreeterUserPicker {
id: userPicker
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: root.userListOpen ? undefined : parent.verticalCenter
anchors.top: root.userListOpen ? parent.top : undefined
anchors.margins: Theme.spacingM
maxExpandedHeight: root.userPickerMaxHeight
visible: root.showUserPicker && !GreeterState.showPasswordInput
expanded: root.userListOpen
onUserSelected: username => root.selectUser(username, false)
onToggleRequested: root.userListOpen = !root.userListOpen
}
DankIcon { DankIcon {
id: lockIcon id: lockIcon
@@ -1097,7 +916,6 @@ Item {
name: GreeterState.showPasswordInput ? "lock" : "person" name: GreeterState.showPasswordInput ? "lock" : "person"
size: 20 size: 20
color: inputField.activeFocus ? Theme.primary : Theme.surfaceVariantText color: inputField.activeFocus ? Theme.primary : Theme.surfaceVariantText
visible: !root.showUserPicker
} }
TextInput { TextInput {
@@ -1123,9 +941,8 @@ Item {
} }
return margin; return margin;
} }
enabled: !root.showUserPicker || GreeterState.showPasswordInput
opacity: 0 opacity: 0
focus: !root.showUserPicker || GreeterState.showPasswordInput focus: true
echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal
onTextChanged: { onTextChanged: {
if (syncingFromState) if (syncingFromState)
@@ -1188,14 +1005,11 @@ Item {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
return I18n.tr("Password..."); return I18n.tr("Password...");
} }
if (root.showUserPicker) {
return "";
}
return I18n.tr("Username..."); return I18n.tr("Username...");
} }
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : (root.showUserPicker ? false : GreeterState.usernameInput.length === 0)) ? 1 : 0 opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
@@ -1229,7 +1043,7 @@ Item {
} }
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : (root.showUserPicker ? false : GreeterState.usernameInput.length > 0)) ? 1 : 0 opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : GreeterState.usernameInput.length > 0) ? 1 : 0
clip: true clip: true
elide: Text.ElideNone elide: Text.ElideNone
horizontalAlignment: implicitWidth > width ? Text.AlignRight : Text.AlignLeft horizontalAlignment: implicitWidth > width ? Text.AlignRight : Text.AlignLeft
@@ -1274,7 +1088,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard" iconName: "keyboard"
buttonSize: 32 buttonSize: 32
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking && (!root.showUserPicker || GreeterState.showPasswordInput) visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: { onClicked: {
if (keyboard_controller.isKeyboardActive) { if (keyboard_controller.isKeyboardActive) {
@@ -1293,7 +1107,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return" iconName: "keyboard_return"
buttonSize: 36 buttonSize: 36
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking && (!root.showUserPicker || GreeterState.showPasswordInput) visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: true enabled: true
onClicked: { onClicked: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
@@ -1323,36 +1137,6 @@ Item {
} }
} }
Item {
Layout.fillWidth: true
Layout.preferredHeight: root.showAccountSwitchLink ? 28 : 0
visible: root.showAccountSwitchLink
StyledText {
id: accountSwitchLabel
anchors.horizontalCenter: parent.horizontalCenter
text: root.manualUsernameEntry ? I18n.tr("Back to user list", "greeter link to return from manual username entry to user picker") : I18n.tr("Not listed?", "greeter link to switch to manual username entry")
color: Theme.primary
font.pixelSize: Theme.fontSizeSmall
font.underline: accountSwitchMouse.containsMouse
}
MouseArea {
id: accountSwitchMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.manualUsernameEntry)
root.returnToUserListFromManualEntry();
else
root.enterManualUsernameEntry();
}
}
}
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 38 Layout.preferredHeight: 38
@@ -1414,8 +1198,13 @@ Item {
StateLayer { StateLayer {
stateColor: Theme.primary stateColor: Theme.primary
cornerRadius: parent.radius cornerRadius: parent.radius
enabled: !GreeterState.unlocking && GreeterState.showPasswordInput enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: root.returnToUserPicker() onClicked: {
GreeterState.reset();
root.externalAuthAutoStartedForUser = "";
inputField.text = "";
PortalService.profileImage = "";
}
} }
} }
} }
@@ -19,8 +19,6 @@ Singleton {
property var sessionExecs: [] property var sessionExecs: []
property var sessionPaths: [] property var sessionPaths: []
property int currentSessionIndex: 0 property int currentSessionIndex: 0
property var availableUsers: []
property int selectedUserIndex: -1
function reset() { function reset() {
showPasswordInput = false; showPasswordInput = false;
@@ -28,6 +26,5 @@ Singleton {
usernameInput = ""; usernameInput = "";
passwordBuffer = ""; passwordBuffer = "";
pamState = ""; pamState = "";
selectedUserIndex = -1;
} }
} }
@@ -1,155 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool expanded: false
property int maxExpandedHeight: 400
signal userSelected(string username)
signal toggleRequested()
readonly property int rowHeight: 52
readonly property int collapsedBarHeight: 36
readonly property int expandedListHeight: {
if (!expanded)
return 0;
const count = GreeterUsersService.users.length;
if (count === 0)
return 0;
const fullHeight = count * rowHeight + Math.max(0, count - 1) * Theme.spacingXS;
return Math.min(maxExpandedHeight, fullHeight);
}
function encodeFileUrl(path) {
if (!path)
return "";
return "file://" + path.split("/").map(s => encodeURIComponent(s)).join("/");
}
function profileImageSource(username) {
const path = GreeterUsersService.profileImagePath(username);
if (path)
return encodeFileUrl(path);
return "";
}
implicitHeight: expanded ? expandedListHeight : collapsedBarHeight
implicitWidth: parent ? parent.width : 320
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: expanded ? undefined : parent.verticalCenter
height: collapsedBarHeight
visible: !expanded && !!GreeterState.username
spacing: Theme.spacingM
StyledText {
Layout.fillWidth: true
text: GreeterUsersService.optionLabel(GreeterState.username)
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
elide: Text.ElideRight
}
DankIcon {
name: "expand_more"
size: 20
color: Theme.surfaceVariantText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleRequested()
}
}
Item {
anchors.left: parent.left
anchors.right: parent.right
height: collapsedBarHeight
visible: !expanded && !GreeterState.username
DankIcon {
anchors.centerIn: parent
name: "expand_more"
size: 20
color: Theme.surfaceVariantText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleRequested()
}
}
DankListView {
id: userListView
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: expandedListHeight
visible: expanded
clip: true
interactive: contentHeight > height
spacing: Theme.spacingXS
model: GreeterUsersService.users
delegate: Rectangle {
id: userRow
required property var modelData
required property int index
width: userListView.width
height: root.rowHeight
radius: Theme.cornerRadius
color: userRowMouse.containsMouse ? Theme.surfacePressed : "transparent"
border.color: GreeterState.username === userRow.modelData.username ? Theme.primary : "transparent"
border.width: GreeterState.username === userRow.modelData.username ? 1 : 0
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingM
Item {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
DankCircularImage {
anchors.fill: parent
imageSource: root.profileImageSource(userRow.modelData.username)
fallbackIcon: "person"
}
}
StyledText {
Layout.fillWidth: true
text: GreeterUsersService.optionLabel(userRow.modelData.username)
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
elide: Text.ElideRight
}
}
MouseArea {
id: userRowMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.userSelected(userRow.modelData.username)
}
}
}
}
@@ -1,51 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("GreeterUserTheme")
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string activeUsername: ""
function userCacheDir(username) {
if (!username)
return "";
return greetCfgDir + "/users/" + username;
}
function applyForUser(username) {
const name = (username || "").trim();
activeUsername = name;
if (!name) {
applyDefault();
return;
}
const dir = userCacheDir(name);
if (typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(name)) {
Theme.setGreeterColorsBaseDir(dir);
SessionData.setGreeterSessionBaseDir(dir);
GreetdSettings.setConfigBaseDir(dir);
return;
}
applyDefault();
}
function applyDefault() {
activeUsername = "";
Theme.resetGreeterColorsBaseDir();
SessionData.resetGreeterSessionBaseDir();
GreetdSettings.resetConfigBaseDir();
}
readonly property string activeWallpaperOverridePath: {
const base = activeUsername && typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(activeUsername) ? userCacheDir(activeUsername) : greetCfgDir;
return base ? base + "/greeter_wallpaper_override.jpg" : "";
}
}
+1 -11
View File
@@ -250,17 +250,7 @@ Only niri currently has a generated greeter config path managed by `dms greeter
The greeter can be personalized with wallpapers, themes, weather, clock formats, and more - configured exactly the same as dms. The greeter can be personalized with wallpapers, themes, weather, clock formats, and more - configured exactly the same as dms.
**Easiest method (single user):** Run `dms greeter sync` to automatically sync your DMS theme with the greeter. **Easiest method:** Run `dms greeter sync` to automatically sync your DMS theme with the greeter.
**Multi-user systems:** One **main admin** runs full sync once to set up greetd and the shared cache (`dms greeter sync`, or `dms greeter sync --local` when developing from a checkout). **Every other account**—including other admins—should only run:
```bash
dms greeter sync --profile
```
Before that, an administrator must add each user to the `greeter` group in **Settings → Users** (greeter toggle) or with `sudo usermod -aG greeter <username>`. Each added user must log out and back in before `--profile` will work.
Per-user settings are stored under `/var/cache/dms-greeter/users/<username>/` for the login picker; the root cache remains the default fallback and is owned by whoever ran full sync.
**Manual method:** You can manually synchronize configurations if you want greeter settings to always mirror your shell: **Manual method:** You can manually synchronize configurations if you want greeter settings to always mirror your shell:
+8 -11
View File
@@ -60,7 +60,7 @@ DankOSD {
Image { Image {
id: artPreloader id: artPreloader
source: TrackArtService.resolvedArtUrl source: TrackArtService._bgArtSource
visible: false visible: false
asynchronous: true asynchronous: true
cache: true cache: true
@@ -78,7 +78,7 @@ DankOSD {
function onLoadingChanged() { function onLoadingChanged() {
if (TrackArtService.loading || !root._pendingShow) if (TrackArtService.loading || !root._pendingShow)
return; return;
if (!TrackArtService.resolvedArtUrl || artPreloader.status === Image.Ready) { if (!TrackArtService._bgArtSource || artPreloader.status === Image.Ready) {
root._pendingShow = false; root._pendingShow = false;
root.show(); root.show();
} }
@@ -116,9 +116,9 @@ DankOSD {
root._displayAlbum = player.trackAlbum || ""; root._displayAlbum = player.trackAlbum || "";
root.updatePlaybackIcon(); root.updatePlaybackIcon();
const resolvedArtUrl = TrackArtService.resolvedArtUrl; TrackArtService.loadArtwork(player.trackArtUrl);
if (!resolvedArtUrl || resolvedArtUrl === "") { if (!player.trackArtUrl || player.trackArtUrl === "") {
root.show(); root.show();
return; return;
} }
@@ -126,7 +126,7 @@ DankOSD {
root._pendingShow = true; root._pendingShow = true;
return; return;
} }
if (!TrackArtService.resolvedArtUrl || artPreloader.status === Image.Ready) { if (!TrackArtService._bgArtSource || artPreloader.status === Image.Ready) {
root.show(); root.show();
return; return;
} }
@@ -134,10 +134,7 @@ DankOSD {
} }
function onTrackArtUrlChanged() { function onTrackArtUrlChanged() {
handleUpdate(); TrackArtService.loadArtwork(player.trackArtUrl);
}
function onMetadataChanged() {
handleUpdate();
} }
function onIsPlayingChanged() { function onIsPlayingChanged() {
handleUpdate(); handleUpdate();
@@ -171,14 +168,14 @@ DankOSD {
Item { Item {
id: bgContainer id: bgContainer
anchors.fill: parent anchors.fill: parent
visible: TrackArtService.resolvedArtUrl !== "" visible: TrackArtService._bgArtSource !== ""
Image { Image {
id: bgImage id: bgImage
anchors.centerIn: parent anchors.centerIn: parent
width: Math.max(parent.width, parent.height) width: Math.max(parent.width, parent.height)
height: width height: width
source: TrackArtService.resolvedArtUrl source: TrackArtService._bgArtSource
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: true asynchronous: true
cache: true cache: true
+1 -1
View File
@@ -446,7 +446,7 @@ Item {
settingKey: "greeterStatus" settingKey: "greeterStatus"
StyledText { StyledText {
text: I18n.tr("Check sync status on demand. Sync (full) is for the main admin: it copies your theme to the login screen and sets up system greeter config. On multi-user systems, add other accounts in Settings → Users, then have each of them run dms greeter sync --profile after logging out and back in—not full sync. Authentication changes apply automatically.") text: I18n.tr("Check sync status on demand. Sync copies your theme, settings, and wallpaper configuration to the login screen. Authentication changes apply automatically.")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
+1 -90
View File
@@ -17,14 +17,12 @@ Item {
property string pendingPassword: "" property string pendingPassword: ""
property string pendingConfirm: "" property string pendingConfirm: ""
property bool pendingAdmin: false property bool pendingAdmin: false
property bool pendingGreeter: false
function _resetForm() { function _resetForm() {
pendingUsername = ""; pendingUsername = "";
pendingPassword = ""; pendingPassword = "";
pendingConfirm = ""; pendingConfirm = "";
pendingAdmin = false; pendingAdmin = false;
pendingGreeter = false;
usernameField.text = ""; usernameField.text = "";
passwordField.text = ""; passwordField.text = "";
confirmField.text = ""; confirmField.text = "";
@@ -61,10 +59,6 @@ Item {
id: adminToggleConfirm id: adminToggleConfirm
} }
ConfirmModal {
id: greeterToggleConfirm
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
@@ -118,26 +112,6 @@ Item {
height: 1 height: 1
} }
StyledText {
text: I18n.tr("Greeter group:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: UsersService.greeterGroup
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Theme.spacingM
height: 1
}
StyledText { StyledText {
text: UsersService.refreshing ? I18n.tr("Refreshing…") : "" text: UsersService.refreshing ? I18n.tr("Refreshing…") : ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -146,14 +120,6 @@ Item {
} }
} }
StyledText {
width: parent.width
text: I18n.tr("Greeter group members can sync their login-screen theme with dms greeter sync --profile after logging out and back in.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
}
Repeater { Repeater {
model: UsersService.users model: UsersService.users
@@ -213,24 +179,6 @@ Item {
font.weight: Font.Medium font.weight: Font.Medium
} }
} }
Rectangle {
visible: userRow.modelData.isGreeter
width: greeterChipText.implicitWidth + Theme.spacingS * 2
height: greeterChipText.implicitHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.secondary, 0.15)
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: greeterChipText
anchors.centerIn: parent
text: I18n.tr("greeter")
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondary
font.weight: Font.Medium
}
}
} }
StyledText { StyledText {
@@ -247,34 +195,6 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
DankActionButton {
id: greeterToggleBtn
readonly property bool actionBlocked: root.operationPending
buttonSize: 36
iconSize: 20
iconName: userRow.modelData.isGreeter ? "login" : "how_to_reg"
iconColor: userRow.modelData.isGreeter ? Theme.secondary : Theme.surfaceVariantText
opacity: actionBlocked ? 0.4 : 1.0
tooltipText: userRow.modelData.isGreeter ? I18n.tr("Remove greeter login access") : I18n.tr("Allow greeter login access")
tooltipSide: "left"
onClicked: {
if (actionBlocked)
return;
const enableGreeter = !userRow.modelData.isGreeter;
greeterToggleConfirm.showWithOptions({
title: enableGreeter ? I18n.tr("Allow greeter access?") : I18n.tr("Remove greeter access?"),
message: enableGreeter ? I18n.tr("Add \"%1\" to the %2 group? They must log out and back in, then run dms greeter sync --profile to publish their login-screen theme.").arg(userRow.modelData.username).arg(UsersService.greeterGroup) : I18n.tr("Remove \"%1\" from the %2 group?").arg(userRow.modelData.username).arg(UsersService.greeterGroup),
confirmText: enableGreeter ? I18n.tr("Allow") : I18n.tr("Remove"),
confirmColor: Theme.primary,
onConfirm: () => {
root.operationPending = true;
root.statusText = "";
UsersService.setGreeterAccess(userRow.modelData.username, enableGreeter, null);
}
});
}
}
DankActionButton { DankActionButton {
id: adminToggleBtn id: adminToggleBtn
readonly property bool actionBlocked: root.operationPending || (userRow.isLastAdmin && userRow.modelData.isAdmin) readonly property bool actionBlocked: root.operationPending || (userRow.isLastAdmin && userRow.modelData.isAdmin)
@@ -460,15 +380,6 @@ Item {
onToggled: checked => root.pendingAdmin = checked onToggled: checked => root.pendingAdmin = checked
} }
SettingsToggleRow {
settingKey: "createUserGreeter"
tags: ["user", "greeter", "login", "sync"]
text: I18n.tr("Allow greeter login access")
description: I18n.tr("Add the new user to the %1 group so they can run dms greeter sync --profile.").arg(UsersService.greeterGroup)
checked: root.pendingGreeter
onToggled: checked => root.pendingGreeter = checked
}
Row { Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -484,7 +395,7 @@ Item {
return; return;
root.operationPending = true; root.operationPending = true;
root.statusText = ""; root.statusText = "";
UsersService.createUser(root.pendingUsername, root.pendingPassword, root.pendingAdmin, root.pendingGreeter, null); UsersService.createUser(root.pendingUsername, root.pendingPassword, root.pendingAdmin, null);
} }
} }
+4 -4
View File
@@ -50,8 +50,8 @@ PanelWindow {
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent" color: "transparent"
readonly property real toastWidth: shouldBeVisible ? Theme.px(Math.min(900, messageText.implicitWidth + statusIcon.width + Theme.spacingM + (ToastService.hasDetails ? (expandButton.width + closeButton.width + 4) : (ToastService.currentLevel === ToastService.levelError ? closeButton.width + Theme.spacingS : 0)) + Theme.spacingL * 2 + Theme.spacingM * 2), dpr) : frozenWidth readonly property real toastWidth: shouldBeVisible ? Math.min(900, messageText.implicitWidth + statusIcon.width + Theme.spacingM + (ToastService.hasDetails ? (expandButton.width + closeButton.width + 4) : (ToastService.currentLevel === ToastService.levelError ? closeButton.width + Theme.spacingS : 0)) + Theme.spacingL * 2 + Theme.spacingM * 2) : frozenWidth
readonly property real toastHeight: Theme.px(toastContent.height + Theme.spacingL * 2, dpr) readonly property real toastHeight: toastContent.height + Theme.spacingL * 2
anchors { anchors {
top: true top: true
@@ -63,8 +63,8 @@ PanelWindow {
top: Math.max(0, Theme.snap(toastY - shadowBuffer, dpr)) top: Math.max(0, Theme.snap(toastY - shadowBuffer, dpr))
} }
implicitWidth: Theme.px(toastWidth + (shadowBuffer * 2), dpr) implicitWidth: toastWidth + (shadowBuffer * 2)
implicitHeight: Theme.px(toastHeight + (shadowBuffer * 2), dpr) implicitHeight: toastHeight + (shadowBuffer * 2)
Rectangle { Rectangle {
id: toast id: toast
+11 -8
View File
@@ -236,16 +236,19 @@ Singleton {
readonly property bool suggestPowerSaver: false readonly property bool suggestPowerSaver: false
readonly property var bluetoothDevices: { readonly property var bluetoothDevices: {
const btDevices = [];
const bluetoothTypes = [UPowerDeviceType.BluetoothGeneric, UPowerDeviceType.Headphones, UPowerDeviceType.Headset, UPowerDeviceType.Keyboard, UPowerDeviceType.Mouse, UPowerDeviceType.Speakers]; const bluetoothTypes = [UPowerDeviceType.BluetoothGeneric, UPowerDeviceType.Headphones, UPowerDeviceType.Headset, UPowerDeviceType.Keyboard, UPowerDeviceType.Mouse, UPowerDeviceType.Speakers];
const btDevices = UPower.devices.values.filter(dev => dev && dev.ready && bluetoothTypes.includes(dev.type)).map(dev => { for (var i = 0; i < UPower.devices.count; i++) {
return { const dev = UPower.devices.get(i);
"name": dev.model || UPowerDeviceType.toString(dev.type), if (dev && dev.ready && bluetoothTypes.includes(dev.type)) {
"percentage": Math.round(dev.percentage * 100), btDevices.push({
"type": dev.type "name": dev.model || UPowerDeviceType.toString(dev.type),
}; "percentage": Math.round(dev.percentage * 100),
}); "type": dev.type
});
}
}
return btDevices; return btDevices;
} }
-163
View File
@@ -1,163 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property var log: Log.scoped("GreeterUsersService")
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
readonly property string usersCacheDir: greetCfgDir + "/users"
property var users: []
property var usernames: []
property var profileImageMap: ({})
property bool loaded: false
property bool refreshing: false
Component.onCompleted: refresh()
function refresh() {
if (refreshing)
return;
refreshing = true;
_loadUsers();
}
function displayName(username) {
const u = _findUser(username);
if (!u)
return username || "";
const gecos = (u.gecos || "").trim();
return gecos.length > 0 ? gecos : username;
}
function optionLabel(username) {
const label = displayName(username);
return label !== username ? label : username;
}
function usernameFromOptionLabel(label) {
for (let i = 0; i < users.length; i++) {
if (root.optionLabel(users[i].username) === label)
return users[i].username;
}
return label;
}
function hasSyncedTheme(username) {
if (!username)
return false;
return syncedThemePaths[username] === true;
}
property var syncedThemePaths: ({})
function userCacheDir(username) {
if (!username)
return "";
return usersCacheDir + "/" + username;
}
function syncedSettingsPath(username) {
const dir = userCacheDir(username);
return dir ? dir + "/settings.json" : "";
}
function _findUser(name) {
for (let i = 0; i < users.length; i++) {
if (users[i].username === name)
return users[i];
}
return null;
}
function _loadUsers() {
Proc.runCommand("greeterUsersService-loadUsers", ["sh", "-c", "getent passwd | awk -F: '$3>=1000 && $3<60000 && $1!=\"nobody\" && $7!~/(nologin|false)$/ && $6!=\"/var/empty\" {print $1\":\"$3\":\"$5\":\"$6\":\"$7}'"], (output, exitCode) => {
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
const list = [];
const names = [];
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(":");
if (parts.length < 5)
continue;
const username = parts[0];
list.push({
username,
uid: parseInt(parts[1], 10),
gecos: (parts[2] || "").split(",")[0],
home: parts[3] || "",
shell: parts[4] || ""
});
names.push(username);
}
list.sort((a, b) => a.username.localeCompare(b.username));
names.sort((a, b) => a.localeCompare(b));
root.users = list;
root.usernames = names;
root.loaded = true;
root.refreshing = false;
_refreshSyncedThemeFlags();
_loadProfileIcons();
}, 0);
}
function _refreshSyncedThemeFlags() {
if (usernames.length === 0) {
syncedThemePaths = ({});
return;
}
const checks = usernames.map(u => `[ -f "${syncedSettingsPath(u)}" ] && echo "${u}:1" || echo "${u}:0"`).join("; ");
Proc.runCommand("greeterUsersService-syncedThemes", ["sh", "-c", checks], (output, exitCode) => {
const map = {};
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(":");
if (parts.length >= 2)
map[parts[0]] = parts[1] === "1";
}
root.syncedThemePaths = map;
}, 0);
}
function profileImagePath(username) {
if (!username)
return "";
return profileImageMap[username] || "";
}
function _loadProfileIcons() {
if (users.length === 0) {
profileImageMap = ({});
return;
}
const script = users.map(u => {
const safeUser = u.username.replace(/'/g, "'\\''");
const safeHome = (u.home || "").replace(/'/g, "'\\''");
const cacheDir = usersCacheDir + "/" + u.username;
return `( icon=""; for f in "${cacheDir}/profile.jpg" "${cacheDir}/profile.jpeg" "${cacheDir}/profile.png" "${cacheDir}/profile.webp" "/var/lib/AccountsService/icons/${safeUser}" "${safeHome}/.face" "${safeHome}/.face.icon"; do if [ -f "$f" ] && [ -r "$f" ]; then icon="$f"; break; fi; done; echo "${u.username}:$icon" )`;
}).join("; ");
Proc.runCommand("greeterUsersService-profileIcons", ["sh", "-c", script], (output, exitCode) => {
const map = {};
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
for (let i = 0; i < lines.length; i++) {
const idx = lines[i].indexOf(":");
if (idx <= 0)
continue;
const user = lines[i].substring(0, idx);
const icon = lines[i].substring(idx + 1).trim();
map[user] = icon && icon.length > 0 ? icon : "";
}
for (let j = 0; j < users.length; j++) {
const u = users[j].username;
if (!(u in map))
map[u] = "";
}
root.profileImageMap = map;
}, 0);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ import Quickshell
Singleton { Singleton {
id: root id: root
readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location") readonly property bool locationAvailable: DMSService.isConnected && (DMSService.capabilities.length === 0 || DMSService.capabilities.includes("location"))
readonly property bool valid: latitude !== 0 || longitude !== 0 readonly property bool valid: latitude !== 0 || longitude !== 0
property var latitude: 0.0 property var latitude: 0.0
+4 -38
View File
@@ -11,33 +11,6 @@ Singleton {
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: null property MprisPlayer activePlayer: null
property real activePlayerStableLength: 0
Connections {
target: root.activePlayer
function onTrackTitleChanged() {
root.activePlayerStableLength = (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) ? root.activePlayer.length : 0;
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
}
function onTrackArtistChanged() {
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
}
function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length;
}
}
function onPlaybackStateChanged() {
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
}
}
onActivePlayerChanged: {
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
}
onAvailablePlayersChanged: _resolveActivePlayer() onAvailablePlayersChanged: _resolveActivePlayer()
Component.onCompleted: _resolveActivePlayer() Component.onCompleted: _resolveActivePlayer()
@@ -54,13 +27,6 @@ Singleton {
} }
} }
function isIdle(player: MprisPlayer): bool {
return player
&& player.playbackState === MprisPlaybackState.Stopped
&& !player.trackTitle
&& !player.trackArtist;
}
function _resolveActivePlayer(): void { function _resolveActivePlayer(): void {
const playing = availablePlayers.find(p => p.isPlaying); const playing = availablePlayers.find(p => p.isPlaying);
if (playing) { if (playing) {
@@ -68,17 +34,17 @@ Singleton {
_persistIdentity(playing.identity); _persistIdentity(playing.identity);
return; return;
} }
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0 && !isIdle(activePlayer)) if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
return; return;
const savedId = SessionData.lastPlayerIdentity; const savedId = SessionData.lastPlayerIdentity;
if (savedId) { if (savedId) {
const match = availablePlayers.find(p => p.identity === savedId); const match = availablePlayers.find(p => p.identity === savedId);
if (match && !isIdle(match)) { if (match) {
activePlayer = match; activePlayer = match;
return; return;
} }
} }
activePlayer = availablePlayers.find(p => p.canControl && !isIdle(p)) ?? null; activePlayer = availablePlayers.find(p => p.canControl && p.canPlay) ?? null;
if (activePlayer) if (activePlayer)
_persistIdentity(activePlayer.identity); _persistIdentity(activePlayer.identity);
} }
@@ -115,7 +81,7 @@ Singleton {
if (!activePlayer) if (!activePlayer)
return; return;
if (activePlayer.position > 8 && activePlayer.canSeek) if (activePlayer.position > 8 && activePlayer.canSeek)
activePlayer.position = 0.1; activePlayer.position = 0;
else if (activePlayer.canGoPrevious) else if (activePlayer.canGoPrevious)
activePlayer.previous(); activePlayer.previous();
} }
+1 -15
View File
@@ -239,23 +239,11 @@ Singleton {
}); });
} }
property string pendingGreeterProfileUser: ""
function getGreeterUserProfileImage(username) { function getGreeterUserProfileImage(username) {
if (!username) { if (!username) {
profileImage = ""; profileImage = "";
pendingGreeterProfileUser = "";
return; return;
} }
if (typeof GreeterUsersService !== "undefined") {
const cachedPath = GreeterUsersService.profileImagePath(username);
if (cachedPath) {
profileImage = cachedPath;
pendingGreeterProfileUser = "";
return;
}
}
pendingGreeterProfileUser = username;
userProfileCheckProcess.command = ["bash", "-c", `uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`]; userProfileCheckProcess.command = ["bash", "-c", `uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`];
userProfileCheckProcess.running = true; userProfileCheckProcess.running = true;
} }
@@ -273,14 +261,12 @@ Singleton {
} else { } else {
root.profileImage = ""; root.profileImage = "";
} }
root.pendingGreeterProfileUser = "";
} }
} }
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0 && root.pendingGreeterProfileUser !== "") { if (exitCode !== 0) {
root.profileImage = ""; root.profileImage = "";
root.pendingGreeterProfileUser = "";
} }
} }
} }
+8 -123
View File
@@ -10,53 +10,12 @@ Singleton {
id: root id: root
property string _lastArtUrl: "" property string _lastArtUrl: ""
property string resolvedArtUrl: "" property string _bgArtSource: ""
property alias _bgArtSource: root.resolvedArtUrl
property bool loading: false property bool loading: false
function djb2Hash(str) {
if (!str) return "";
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i);
hash = hash & 0x7FFFFFFF;
}
return hash.toString(16).padStart(8, '0');
}
function getArtworkUrl(player) {
if (!player) return "";
// 1. If native trackArtUrl is present and valid
let artUrl = player.trackArtUrl || "";
if (artUrl !== "") {
return artUrl;
}
// 2. Fallback to raw metadata mpris:artUrl if present
if (player.metadata && player.metadata["mpris:artUrl"]) {
artUrl = player.metadata["mpris:artUrl"].toString();
if (artUrl !== "") return artUrl;
}
// 3. Fallback for YouTube from xesam:url
if (player.metadata && player.metadata["xesam:url"]) {
const url = player.metadata["xesam:url"].toString();
if (url.includes("youtube.com") || url.includes("youtu.be")) {
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
const match = url.match(regExp);
if (match && match[2].length === 11) {
return "https://img.youtube.com/vi/" + match[2] + "/hqdefault.jpg";
}
}
}
return "";
}
function loadArtwork(url) { function loadArtwork(url) {
if (!url || url === "") { if (!url || url === "") {
resolvedArtUrl = ""; _bgArtSource = "";
_lastArtUrl = ""; _lastArtUrl = "";
loading = false; loading = false;
return; return;
@@ -66,99 +25,25 @@ Singleton {
_lastArtUrl = url; _lastArtUrl = url;
if (url.startsWith("http://") || url.startsWith("https://")) { if (url.startsWith("http://") || url.startsWith("https://")) {
loading = true; _bgArtSource = url;
resolvedArtUrl = ""; // Clear stale artwork immediately while loading loading = false;
const targetUrl = url;
const hash = djb2Hash(url);
const cacheDir = Paths.strip(Paths.imagecache);
const filePath = cacheDir + "/remote_" + hash;
const localFileUrl = "file://" + filePath;
// 1. First, check if the file already exists locally
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== targetUrl)
return;
if (exitCode === 0) {
resolvedArtUrl = localFileUrl;
loading = false;
} else {
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
// 2. Check if this is a YouTube URL to do high quality 16:9 fallback
if (targetUrl.includes("img.youtube.com/vi/")) {
const videoId = targetUrl.split("/vi/")[1].split("/")[0];
const maxresUrl = "https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg";
const mqUrl = "https://img.youtube.com/vi/" + videoId + "/mqdefault.jpg";
const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, maxresUrl, filePath], (maxOutput, maxExitCode) => {
if (_lastArtUrl !== targetUrl)
return;
if (maxExitCode === 0) {
resolvedArtUrl = localFileUrl;
loading = false;
} else {
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
if (_lastArtUrl !== targetUrl)
return;
if (mqExitCode === 0) {
resolvedArtUrl = localFileUrl;
} else {
resolvedArtUrl = targetUrl; // Ultimate fallback
}
loading = false;
}, 50, 15000);
}
}, 50, 15000);
} else {
// Standard curl download for other remote URLs (e.g. SoundCloud)
const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
if (_lastArtUrl !== targetUrl)
return;
if (dlExitCode === 0) {
resolvedArtUrl = localFileUrl;
} else {
resolvedArtUrl = targetUrl; // Fallback to raw URL
}
loading = false;
}, 50, 15000);
}
}
}, 50, 5000);
return; return;
} }
loading = true; loading = true;
resolvedArtUrl = ""; // Clear stale artwork immediately while verifying local file
const localUrl = url; const localUrl = url;
const filePath = url.startsWith("file://") ? url.substring(7) : url; const filePath = url.startsWith("file://") ? url.substring(7) : url;
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => { Proc.runCommand("trackart", ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== localUrl) if (_lastArtUrl !== localUrl)
return; return;
resolvedArtUrl = exitCode === 0 ? localUrl : ""; _bgArtSource = exitCode === 0 ? localUrl : "";
loading = false; loading = false;
}, 200); }, 200);
} }
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
onActivePlayerChanged: _updateArtUrl() onActivePlayerChanged: {
loadArtwork(activePlayer?.trackArtUrl ?? "");
Connections {
target: root.activePlayer
ignoreUnknownSignals: true
function onTrackTitleChanged() { root._updateArtUrl(); }
function onTrackArtUrlChanged() { root._updateArtUrl(); }
function onMetadataChanged() { root._updateArtUrl(); }
}
function _updateArtUrl() {
const url = getArtworkUrl(activePlayer);
loadArtwork(url);
} }
} }
+16 -120
View File
@@ -12,9 +12,7 @@ Singleton {
property var users: [] property var users: []
property string adminGroup: "wheel" property string adminGroup: "wheel"
property string greeterGroup: "greeter"
property var adminMembers: [] property var adminMembers: []
property var greeterMembers: []
property bool refreshing: false property bool refreshing: false
signal operationCompleted(string op, string username, bool success, string message) signal operationCompleted(string op, string username, bool success, string message)
@@ -71,35 +69,17 @@ Singleton {
Proc.runCommand("usersService-adminMembers", ["sh", "-c", "getent group " + root.adminGroup + " | awk -F: '{print $4}'"], (output, exitCode) => { Proc.runCommand("usersService-adminMembers", ["sh", "-c", "getent group " + root.adminGroup + " | awk -F: '{print $4}'"], (output, exitCode) => {
const members = (output || "").trim().split(",").map(s => s.trim()).filter(s => s.length > 0); const members = (output || "").trim().split(",").map(s => s.trim()).filter(s => s.length > 0);
root.adminMembers = members; root.adminMembers = members;
_detectGreeterGroup();
}, 0);
}
function _detectGreeterGroup() {
Proc.runCommand("usersService-detectGreeterGroup", ["sh", "-c", "getent group greeter >/dev/null 2>&1 && echo greeter || (getent group greetd >/dev/null 2>&1 && echo greetd || (getent group _greeter >/dev/null 2>&1 && echo _greeter || echo greeter))"], (output, exitCode) => {
root.greeterGroup = (output || "").trim() || "greeter";
_loadGreeterMembers();
}, 0);
}
function _loadGreeterMembers() {
Proc.runCommand("usersService-greeterMembers", ["sh", "-c", "getent group " + root.greeterGroup + " 2>/dev/null | awk -F: '{print $4}'"], (output, exitCode) => {
const members = (output || "").trim().split(",").map(s => s.trim()).filter(s => s.length > 0);
root.greeterMembers = members;
_loadUsers(); _loadUsers();
}, 0); }, 0);
} }
function _loadUsers() { function _loadUsers() {
Proc.runCommand("usersService-loadUsers", ["sh", "-c", "getent passwd | awk -F: '$3>=1000 && $3<60000 && $1!=\"nobody\" && $7!~/(nologin|false)$/ && $6!=\"/var/empty\" {print $1\":\"$3\":\"$5\":\"$6\":\"$7}'"], (output, exitCode) => { Proc.runCommand("usersService-loadUsers", ["sh", "-c", "getent passwd | awk -F: '$3>=1000 && $3<60000 && $1!=\"nobody\" {print $1\":\"$3\":\"$5\":\"$6\":\"$7}'"], (output, exitCode) => {
const lines = (output || "").trim().split("\n").filter(l => l.length > 0); const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
const list = []; const list = [];
const adminSet = {}; const adminSet = {};
const greeterSet = {};
for (let i = 0; i < root.adminMembers.length; i++) for (let i = 0; i < root.adminMembers.length; i++)
adminSet[root.adminMembers[i]] = true; adminSet[root.adminMembers[i]] = true;
for (let i = 0; i < root.greeterMembers.length; i++)
greeterSet[root.greeterMembers[i]] = true;
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(":"); const parts = lines[i].split(":");
@@ -112,8 +92,7 @@ Singleton {
gecos: (parts[2] || "").split(",")[0], gecos: (parts[2] || "").split(",")[0],
home: parts[3] || "", home: parts[3] || "",
shell: parts[4] || "", shell: parts[4] || "",
isAdmin: adminSet[username] === true, isAdmin: adminSet[username] === true
isGreeter: greeterSet[username] === true
}); });
} }
list.sort((a, b) => a.username.localeCompare(b.username)); list.sort((a, b) => a.username.localeCompare(b.username));
@@ -122,7 +101,7 @@ Singleton {
}, 0); }, 0);
} }
function createUser(username, password, addToAdmin, addToGreeter, callback) { function createUser(username, password, addToAdmin, callback) {
if (!isValidUsername(username)) { if (!isValidUsername(username)) {
_emit("create", username, false, I18n.tr("Invalid username"), callback); _emit("create", username, false, I18n.tr("Invalid username"), callback);
return; return;
@@ -135,7 +114,7 @@ Singleton {
_emit("create", username, false, I18n.tr("User already exists"), callback); _emit("create", username, false, I18n.tr("User already exists"), callback);
return; return;
} }
_runUseradd(username, password, addToAdmin === true, addToGreeter === true, callback); _runUseradd(username, password, addToAdmin === true, callback);
} }
function setPassword(username, newPassword, callback) { function setPassword(username, newPassword, callback) {
@@ -177,55 +156,6 @@ Singleton {
_runAdminToggle(username, makeAdmin === true, callback); _runAdminToggle(username, makeAdmin === true, callback);
} }
function setGreeterAccess(username, enable, callback) {
if (!userExists(username)) {
_emit("greeter", username, false, I18n.tr("User not found"), callback);
return;
}
_runGreeterToggle(username, enable === true, callback);
}
function _finishCreateUser(targetUser, addAdmin, addGreeter, outerCb) {
function finish(success, message) {
root._emit("create", targetUser, success, message, outerCb);
}
function maybeGreeter(onDone) {
if (addGreeter) {
root._runGreeterToggle(targetUser, true, (greeterOk, greeterMsg) => {
if (greeterOk)
onDone();
else
finish(false, greeterMsg);
});
} else {
onDone();
}
}
function createMessage() {
if (addAdmin && addGreeter)
return I18n.tr("User created with administrator and greeter login access");
if (addAdmin)
return I18n.tr("User created with administrator privileges");
if (addGreeter)
return I18n.tr("User created with greeter login access");
return I18n.tr("User created");
}
if (addAdmin) {
root._runAdminToggle(targetUser, true, (adminOk, adminMsg) => {
if (!adminOk) {
finish(false, adminMsg);
return;
}
maybeGreeter(() => finish(true, createMessage()));
});
} else {
maybeGreeter(() => finish(true, createMessage()));
}
}
function _emit(op, username, success, message, callback) { function _emit(op, username, success, message, callback) {
root.operationCompleted(op, username, success, message); root.operationCompleted(op, username, success, message);
if (typeof callback === "function") { if (typeof callback === "function") {
@@ -244,7 +174,6 @@ Singleton {
property string targetUser: "" property string targetUser: ""
property string targetPassword: "" property string targetPassword: ""
property bool addAdmin: false property bool addAdmin: false
property bool addGreeter: false
property var cb: null property var cb: null
property string capturedErr: "" property string capturedErr: ""
running: false running: false
@@ -262,7 +191,6 @@ Singleton {
const targetUser = useraddProc.targetUser; const targetUser = useraddProc.targetUser;
const targetPassword = useraddProc.targetPassword; const targetPassword = useraddProc.targetPassword;
const addAdmin = useraddProc.addAdmin; const addAdmin = useraddProc.addAdmin;
const addGreeter = useraddProc.addGreeter;
const outerCb = useraddProc.cb; const outerCb = useraddProc.cb;
Qt.callLater(() => useraddProc.destroy()); Qt.callLater(() => useraddProc.destroy());
@@ -271,7 +199,17 @@ Singleton {
svc._emit("create", targetUser, false, pwMsg, outerCb); svc._emit("create", targetUser, false, pwMsg, outerCb);
return; return;
} }
svc._finishCreateUser(targetUser, addAdmin, addGreeter, outerCb); if (addAdmin) {
svc._runAdminToggle(targetUser, true, (adminOk, adminMsg) => {
if (adminOk) {
svc._emit("create", targetUser, true, I18n.tr("User created with administrator privileges"), outerCb);
} else {
svc._emit("create", targetUser, false, adminMsg, outerCb);
}
});
} else {
svc._emit("create", targetUser, true, I18n.tr("User created"), outerCb);
}
}); });
} }
} }
@@ -352,36 +290,6 @@ Singleton {
} }
} }
Component {
id: greeterToggleComp
Process {
id: greeterToggleProc
property string targetUser: ""
property bool enableGreeter: false
property var cb: null
property string capturedErr: ""
running: false
stdout: StdioCollector {}
stderr: StdioCollector {
onStreamFinished: greeterToggleProc.capturedErr = text || ""
}
onExited: exitCode => {
const targetUser = greeterToggleProc.targetUser;
const enableGreeter = greeterToggleProc.enableGreeter;
const cb = greeterToggleProc.cb;
const err = (greeterToggleProc.capturedErr || "").trim();
Qt.callLater(() => greeterToggleProc.destroy());
if (exitCode !== 0) {
root._emit("greeter", targetUser, false, err || I18n.tr("usermod failed (exit %1)").arg(exitCode), cb);
} else {
root.refresh();
root._emit("greeter", targetUser, true, enableGreeter ? I18n.tr("Granted greeter login access") : I18n.tr("Removed greeter login access"), cb);
}
}
}
}
Component { Component {
id: adminToggleComp id: adminToggleComp
Process { Process {
@@ -412,13 +320,12 @@ Singleton {
} }
} }
function _runUseradd(username, password, addToAdmin, addToGreeter, callback) { function _runUseradd(username, password, addToAdmin, callback) {
const proc = useraddComp.createObject(root, { const proc = useraddComp.createObject(root, {
command: ["pkexec", "useradd", "-m", "-s", "/bin/bash", username], command: ["pkexec", "useradd", "-m", "-s", "/bin/bash", username],
targetUser: username, targetUser: username,
targetPassword: password, targetPassword: password,
addAdmin: addToAdmin, addAdmin: addToAdmin,
addGreeter: addToGreeter,
cb: callback cb: callback
}); });
proc.running = true; proc.running = true;
@@ -454,16 +361,5 @@ Singleton {
proc.running = true; proc.running = true;
} }
function _runGreeterToggle(username, enableGreeter, callback) {
const cmd = enableGreeter ? ["pkexec", "usermod", "-aG", root.greeterGroup, username] : ["pkexec", "gpasswd", "-d", username, root.greeterGroup];
const proc = greeterToggleComp.createObject(root, {
command: cmd,
targetUser: username,
enableGreeter: enableGreeter,
cb: callback
});
proc.running = true;
}
Component.onCompleted: refresh() Component.onCompleted: refresh()
} }
+2 -6
View File
@@ -8,19 +8,15 @@ Item {
id: root id: root
property MprisPlayer activePlayer property MprisPlayer activePlayer
property string artUrl: TrackArtService.resolvedArtUrl property string artUrl: (activePlayer?.trackArtUrl) || ""
property string lastValidArtUrl: "" property string lastValidArtUrl: ""
property alias albumArtStatus: albumArt.imageStatus property alias albumArtStatus: albumArt.imageStatus
property real albumSize: Math.min(width, height) * 0.88 property real albumSize: Math.min(width, height) * 0.88
property bool showAnimation: true property bool showAnimation: true
property real animationScale: 1.0 property real animationScale: 1.0
onActivePlayerChanged: {
lastValidArtUrl = "";
}
onArtUrlChanged: { onArtUrlChanged: {
if (artUrl && albumArtStatus !== Image.Error) { if (artUrl && albumArt.status !== Image.Error) {
lastValidArtUrl = artUrl; lastValidArtUrl = artUrl;
} }
} }
+24 -28
View File
@@ -58,30 +58,6 @@ Item {
dropdownMenu.close(); dropdownMenu.close();
} }
function openDropdownMenu() {
if (dropdownMenu.visible) {
dropdownMenu.close();
return;
}
if (root.options.length === 0)
return;
dropdownMenu.open();
let currentIndex = root.options.indexOf(root.currentValue);
listView.positionViewAtIndex(currentIndex >= 0 ? currentIndex : 0, ListView.Beginning);
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0);
const popupW = dropdownMenu.width;
const popupH = dropdownMenu.height;
const overlayH = Overlay.overlay.height;
const goUp = root.openUpwards || pos.y + dropdown.height + popupH + 4 > overlayH;
dropdownMenu.x = root.alignPopupRight ? pos.x + dropdown.width - popupW : pos.x - (root.popupWidthOffset / 2);
dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + dropdown.height + 4;
if (root.enableFuzzySearch)
searchField.forceActiveFocus();
}
function resetSearch() { function resetSearch() {
searchField.text = ""; searchField.text = "";
dropdownMenu.fzfFinder = null; dropdownMenu.fzfFinder = null;
@@ -147,7 +123,27 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.openDropdownMenu() onClicked: {
if (dropdownMenu.visible) {
dropdownMenu.close();
return;
}
dropdownMenu.open();
let currentIndex = root.options.indexOf(root.currentValue);
listView.positionViewAtIndex(currentIndex, ListView.Beginning);
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0);
const popupW = dropdownMenu.width;
const popupH = dropdownMenu.height;
const overlayH = Overlay.overlay.height;
const goUp = root.openUpwards || pos.y + dropdown.height + popupH + 4 > overlayH;
dropdownMenu.x = root.alignPopupRight ? pos.x + dropdown.width - popupW : pos.x - (root.popupWidthOffset / 2);
dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + dropdown.height + 4;
if (root.enableFuzzySearch)
searchField.forceActiveFocus();
}
} }
Row { Row {
@@ -169,10 +165,10 @@ Item {
} }
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter text: root.currentValue
text: root.currentValue !== "" ? root.currentValue : root.emptyText
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: root.currentValue !== "" ? Theme.surfaceText : Theme.outline color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0) width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0)
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
+17 -19
View File
@@ -8,14 +8,12 @@ Item {
id: root id: root
property MprisPlayer activePlayer property MprisPlayer activePlayer
readonly property real stableLength: MprisController.activePlayerStableLength
property real seekPreviewRatio: -1 property real seekPreviewRatio: -1
readonly property real playerValue: { readonly property real playerValue: {
if (!activePlayer || stableLength <= 0) if (!activePlayer || activePlayer.length <= 0)
return 0; return 0;
const pos = (activePlayer.position || 0) % Math.max(1, stableLength); const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
const calculatedRatio = pos / stableLength; const calculatedRatio = pos / activePlayer.length;
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue
@@ -31,20 +29,20 @@ Item {
} }
function ratioForPosition(position) { function ratioForPosition(position) {
if (!activePlayer || stableLength <= 0) if (!activePlayer || activePlayer.length <= 0)
return 0; return 0;
return clampRatio(position / stableLength); return clampRatio(position / activePlayer.length);
} }
function positionForRatio(ratio) { function positionForRatio(ratio) {
if (!activePlayer || stableLength <= 0) if (!activePlayer || activePlayer.length <= 0)
return 0; return 0;
const rawPosition = clampRatio(ratio) * stableLength; const rawPosition = clampRatio(ratio) * activePlayer.length;
return Math.min(rawPosition, stableLength * 0.99); return Math.min(rawPosition, activePlayer.length * 0.99);
} }
function updatePreviewFromMouse(mouseX, width) { function updatePreviewFromMouse(mouseX, width) {
if (!activePlayer || stableLength <= 0 || width <= 0) if (!activePlayer || activePlayer.length <= 0 || width <= 0)
return; return;
seekPreviewRatio = clampRatio(mouseX / width); seekPreviewRatio = clampRatio(mouseX / width);
} }
@@ -70,7 +68,7 @@ Item {
mouseArea.pressX = mouse.x; mouseArea.pressX = mouse.x;
clearCommittedSeekPreview(); clearCommittedSeekPreview();
holdTimer.restart(); holdTimer.restart();
if (activePlayer && stableLength > 0 && activePlayer.canSeek) { if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
updatePreviewFromMouse(mouse.x, width); updatePreviewFromMouse(mouse.x, width);
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio); mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
} }
@@ -80,9 +78,9 @@ Item {
holdTimer.stop(); holdTimer.stop();
isSeeking = false; isSeeking = false;
isDraggingSeek = false; isDraggingSeek = false;
if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && stableLength > 0) { if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(mouseArea.pendingSeekPosition, stableLength * 0.99); const clamped = Math.min(mouseArea.pendingSeekPosition, activePlayer.length * 0.99);
activePlayer.position = Math.max(0.1, clamped); activePlayer.position = clamped;
mouseArea.pendingSeekPosition = -1; mouseArea.pendingSeekPosition = -1;
beginCommittedSeekPreview(clamped); beginCommittedSeekPreview(clamped);
} else { } else {
@@ -91,7 +89,7 @@ Item {
} }
function handleSeekPositionChanged(mouse, width, mouseArea) { function handleSeekPositionChanged(mouse, width, mouseArea) {
if (mouseArea.pressed && isSeeking && activePlayer && stableLength > 0 && activePlayer.canSeek) { if (mouseArea.pressed && isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold) if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold)
isDraggingSeek = true; isDraggingSeek = true;
updatePreviewFromMouse(mouse.x, width); updatePreviewFromMouse(mouse.x, width);
@@ -131,7 +129,7 @@ Item {
Loader { Loader {
anchors.fill: parent anchors.fill: parent
visible: activePlayer && stableLength > 0 visible: activePlayer && activePlayer.length > 0
sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent
z: 1 z: 1
@@ -150,7 +148,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && stableLength > 0 enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1 property real pendingSeekPosition: -1
property real pressX: 0 property real pressX: 0
@@ -238,7 +236,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && stableLength > 0 enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1 property real pendingSeekPosition: -1
property real pressX: 0 property real pressX: 0
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
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
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
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
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -735,21 +735,16 @@
"keywords": [ "keywords": [
"background", "background",
"bar", "bar",
"corner",
"corners", "corners",
"dank", "dank",
"panel", "panel",
"radius", "radius",
"remove",
"round", "round",
"rounded", "rounded",
"rounding",
"statusbar", "statusbar",
"taskbar",
"topbar" "topbar"
], ],
"icon": "rounded_corner", "icon": "rounded_corner"
"description": "Remove corner rounding from the bar"
}, },
{ {
"section": "_tab_3", "section": "_tab_3",
@@ -866,24 +861,18 @@
"category": "Dank Bar", "category": "Dank Bar",
"keywords": [ "keywords": [
"bar", "bar",
"between",
"dank", "dank",
"edges",
"gap", "gap",
"gaps", "gaps",
"margin", "margin",
"margins", "margins",
"padding", "padding",
"panel", "panel",
"screen",
"space",
"spacing", "spacing",
"statusbar", "statusbar",
"taskbar",
"topbar" "topbar"
], ],
"icon": "space_bar", "icon": "space_bar"
"description": "Space between the bar and screen edges"
}, },
{ {
"section": "trayIconTint", "section": "trayIconTint",
@@ -920,20 +909,17 @@
"category": "Dank Bar", "category": "Dank Bar",
"keywords": [ "keywords": [
"alpha", "alpha",
"background",
"bar", "bar",
"dank", "dank",
"opacity", "opacity",
"panel", "panel",
"statusbar", "statusbar",
"taskbar",
"topbar", "topbar",
"translucent", "translucent",
"transparency", "transparency",
"transparent" "transparent"
], ],
"icon": "opacity", "icon": "opacity"
"description": "Opacity of the bar background"
}, },
{ {
"section": "barUseOverlayLayer", "section": "barUseOverlayLayer",
@@ -963,27 +949,25 @@
"keywords": [ "keywords": [
"auto-hide", "auto-hide",
"autohide", "autohide",
"automatically",
"away",
"bar", "bar",
"dank", "dank",
"fullscreen", "fullscreen",
"hidden", "hidden",
"hide", "hide",
"layer", "layer",
"moves",
"overlay", "overlay",
"panel", "panel",
"pointer", "place",
"show", "show",
"statusbar", "statusbar",
"taskbar", "taskbar",
"topbar", "topbar",
"visibility", "visibility",
"visible" "visible",
"wayland"
], ],
"icon": "visibility_off", "icon": "visibility_off",
"description": "Automatically hide the bar when the pointer moves away" "description": "Place the bar on the Wayland overlay layer"
}, },
{ {
"section": "workspaceDragReorder", "section": "workspaceDragReorder",
@@ -2347,30 +2331,6 @@
], ],
"icon": "history" "icon": "history"
}, },
{
"section": "rememberLastMode",
"label": "Remember Last Mode",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"app drawer",
"app menu",
"applications",
"drawer",
"last",
"launcher",
"menu",
"mode",
"opened",
"remember",
"restore",
"selected",
"start",
"start menu",
"tab"
],
"description": "Restore the last selected mode (tab) when the launcher is opened"
},
{ {
"section": "rememberLastQuery", "section": "rememberLastQuery",
"label": "Remember Last Query", "label": "Remember Last Query",
@@ -4903,6 +4863,27 @@
], ],
"description": "Automatically lock the screen when DMS starts" "description": "Automatically lock the screen when DMS starts"
}, },
{
"section": "lockBeforeSuspend",
"label": "Lock before suspend",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"automatic",
"automatically",
"before",
"lock",
"login",
"password",
"prepares",
"screen",
"security",
"sleep",
"suspend",
"system"
],
"description": "Automatically lock the screen when the system prepares to suspend"
},
{ {
"section": "lockScreenNotificationMode", "section": "lockScreenNotificationMode",
"label": "Notification Display", "label": "Notification Display",
@@ -5826,6 +5807,28 @@
], ],
"description": "Use smaller notification cards" "description": "Use smaller notification cards"
}, },
{
"section": "notificationDedupeEnabled",
"label": "Suppress Duplicate Notifications",
"tabIndex": 17,
"category": "Notifications",
"keywords": [
"alert",
"alerts",
"coalesce",
"dedupe",
"duplicate",
"duplicates",
"messages",
"notif",
"notification",
"notifications",
"repeat",
"stack",
"toast"
],
"description": "Control whether identical alerts stack or show as a single popup"
},
{ {
"section": "notificationHistorySaveCritical", "section": "notificationHistorySaveCritical",
"label": "Critical Priority", "label": "Critical Priority",
@@ -6366,28 +6369,6 @@
], ],
"description": "Hide notification content until expanded; popups show collapsed by default" "description": "Hide notification content until expanded; popups show collapsed by default"
}, },
{
"section": "notificationDedupeEnabled",
"label": "Suppress Duplicate Notifications",
"tabIndex": 17,
"category": "Notifications",
"keywords": [
"alert",
"alerts",
"coalesce",
"dedupe",
"duplicate",
"messages",
"notif",
"notification",
"notifications",
"notifs",
"repeat",
"stack",
"suppress",
"toast"
]
},
{ {
"section": "osdAlwaysShowValue", "section": "osdAlwaysShowValue",
"label": "Always Show Percentage", "label": "Always Show Percentage",
@@ -7031,27 +7012,6 @@
"icon": "schedule", "icon": "schedule",
"description": "Gradually fade the screen before locking with a configurable grace period" "description": "Gradually fade the screen before locking with a configurable grace period"
}, },
{
"section": "lockBeforeSuspend",
"label": "Lock before suspend",
"tabIndex": 21,
"category": "Power & Sleep",
"keywords": [
"automatically",
"before",
"energy",
"lock",
"power",
"prepares",
"screen",
"security",
"shutdown",
"sleep",
"suspend",
"system"
],
"description": "Automatically lock the screen when the system prepares to suspend"
},
{ {
"section": "fadeToLockGracePeriod", "section": "fadeToLockGracePeriod",
"label": "Lock fade grace period", "label": "Lock fade grace period",
@@ -7738,16 +7698,5 @@
"settings" "settings"
], ],
"icon": "apps" "icon": "apps"
},
{
"section": "_tab_35",
"label": "Users",
"tabIndex": 35,
"category": "Settings",
"keywords": [
"settings",
"users"
],
"icon": "manage_accounts"
} }
] ]
File diff suppressed because it is too large Load Diff