mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 11:38:30 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d91481c464 |
+2
-5
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
|
||||
Command-line interface and daemon for shell management and system control.
|
||||
|
||||
**dankinstall**
|
||||
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
|
||||
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
|
||||
|
||||
## System Integration
|
||||
|
||||
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
|
||||
|
||||
## Supported Distributions
|
||||
|
||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives)
|
||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
||||
|
||||
**Arch Linux**
|
||||
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
||||
@@ -214,7 +214,4 @@ Most packages available in standard repos. Minimal building required.
|
||||
**Gentoo**
|
||||
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
|
||||
|
||||
**Void Linux**
|
||||
Uses XBPS with the DMS and DankLinux self-hosted repositories.
|
||||
|
||||
See installer output for distribution-specific details during installation.
|
||||
|
||||
@@ -23,8 +23,6 @@ var (
|
||||
replaceConfigs []string
|
||||
replaceConfigsAll bool
|
||||
yes bool
|
||||
danksearch bool
|
||||
dankcalendar bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -51,8 +49,6 @@ func init() {
|
||||
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
|
||||
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
|
||||
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
||||
rootCmd.Flags().BoolVar(&danksearch, "danksearch", false, "Install danksearch and enable its user indexing service")
|
||||
rootCmd.Flags().BoolVar(&dankcalendar, "dankcalendar", false, "Install dankcalendar")
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -78,8 +74,6 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
|
||||
"replace-configs",
|
||||
"replace-configs-all",
|
||||
"yes",
|
||||
"danksearch",
|
||||
"dankcalendar",
|
||||
}
|
||||
var set []string
|
||||
for _, name := range headlessOnly {
|
||||
@@ -115,8 +109,6 @@ func runHeadless() error {
|
||||
ReplaceConfigs: replaceConfigs,
|
||||
ReplaceConfigsAll: replaceConfigsAll,
|
||||
Yes: yes,
|
||||
DankSearch: danksearch,
|
||||
DankCalendar: dankcalendar,
|
||||
}
|
||||
|
||||
runner := headless.NewRunner(cfg)
|
||||
|
||||
@@ -886,7 +886,6 @@ func checkOptionalDependencies() []checkResult {
|
||||
{"cava", "cava", "Audio visualizer", true},
|
||||
{"khal", "khal", "Calendar events", false},
|
||||
{"danksearch", "dsearch", "File search", false},
|
||||
{"dankcalendar", "dcal", "Calendar app", false},
|
||||
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
||||
}
|
||||
|
||||
|
||||
@@ -1534,8 +1534,6 @@ func packageInstallHint() string {
|
||||
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
||||
case distros.FamilyArch:
|
||||
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
|
||||
case distros.FamilyVoid:
|
||||
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf)"
|
||||
default:
|
||||
return "Run 'dms greeter install' to install greeter"
|
||||
}
|
||||
@@ -1574,8 +1572,7 @@ func isPackageOnlyGreeterDistro() bool {
|
||||
config.Family == distros.FamilySUSE ||
|
||||
config.Family == distros.FamilyUbuntu ||
|
||||
config.Family == distros.FamilyFedora ||
|
||||
config.Family == distros.FamilyArch ||
|
||||
config.Family == distros.FamilyVoid
|
||||
config.Family == distros.FamilyArch
|
||||
}
|
||||
|
||||
func promptCompositorChoice(compositors []string) (string, error) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
@@ -299,9 +298,6 @@ func runSetup() error {
|
||||
if wmSelected {
|
||||
if wm == deps.WindowManagerMango {
|
||||
useSystemd = false
|
||||
} else if isVoidSetup() {
|
||||
useSystemd = false
|
||||
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
|
||||
} else {
|
||||
useSystemd = promptSystemd()
|
||||
}
|
||||
@@ -376,15 +372,6 @@ func runSetup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isVoidSetup() bool {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
return exists && config.Family == distros.FamilyVoid
|
||||
}
|
||||
|
||||
// Add user to the input group for the evdev manager for inut state tracking.
|
||||
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||
func ensureInputGroup() {
|
||||
|
||||
@@ -582,11 +582,7 @@ func runShellDaemon(session bool) {
|
||||
}
|
||||
|
||||
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "qs", "ipc", "--help")
|
||||
cmd.WaitDelay = 500 * time.Millisecond
|
||||
out, err := cmd.Output()
|
||||
out, err := exec.Command("qs", "ipc", "--help").Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -646,10 +642,7 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
||||
return nil
|
||||
}
|
||||
cmdArgs := append(baseArgs, "show")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "qs", cmdArgs...)
|
||||
cmd.WaitDelay = 500 * time.Millisecond
|
||||
cmd := exec.Command("qs", cmdArgs...)
|
||||
var targets ipcTargets
|
||||
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
|
||||
@@ -61,10 +61,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
|
||||
copy-on-select = false
|
||||
confirm-close-surface = false
|
||||
|
||||
# Disable in-app Ghostty toast notifications
|
||||
app-notifications = false
|
||||
# Disable annoying copied to clipboard
|
||||
app-notifications = no-clipboard-copy,no-config-reload
|
||||
|
||||
# Key bindings for common actions
|
||||
#keybind = ctrl+c=copy_to_clipboard
|
||||
|
||||
@@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
|
||||
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
||||
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
||||
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper"))
|
||||
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
|
||||
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
||||
|
||||
@@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle
|
||||
# Notepad
|
||||
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
||||
# Browse Wallpapers
|
||||
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper
|
||||
bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
|
||||
# Power Menu
|
||||
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
||||
# Cycle Display Profile
|
||||
|
||||
@@ -24,7 +24,7 @@ binds {
|
||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||
}
|
||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper";
|
||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
||||
}
|
||||
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
||||
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||
|
||||
@@ -20,10 +20,3 @@ window-rule {
|
||||
tiled-state true
|
||||
draw-border-with-background false
|
||||
}
|
||||
|
||||
layer-rule {
|
||||
exclude namespace="^dms:bar$"
|
||||
background-effect {
|
||||
xray false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,9 +265,9 @@ recent-windows {
|
||||
}
|
||||
|
||||
// Include dms files
|
||||
include optional=true "dms/colors.kdl"
|
||||
include optional=true "dms/layout.kdl"
|
||||
include optional=true "dms/alttab.kdl"
|
||||
include optional=true "dms/binds.kdl"
|
||||
include optional=true "dms/outputs.kdl"
|
||||
include optional=true "dms/cursor.kdl"
|
||||
include "dms/colors.kdl"
|
||||
include "dms/layout.kdl"
|
||||
include "dms/alttab.kdl"
|
||||
include "dms/binds.kdl"
|
||||
include "dms/outputs.kdl"
|
||||
include "dms/cursor.kdl"
|
||||
|
||||
@@ -119,30 +119,10 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
||||
|
||||
dependencies = append(dependencies, a.detectMatugen())
|
||||
dependencies = append(dependencies, a.detectDgop())
|
||||
dependencies = append(dependencies, a.detectDanksearch())
|
||||
dependencies = append(dependencies, a.detectDankCalendar())
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectDanksearch() deps.Dependency {
|
||||
dep := a.BaseDistribution.detectDanksearch()
|
||||
dep.CanToggle = true
|
||||
if a.packageInstalled("dsearch-git") {
|
||||
dep.Variant = deps.VariantGit
|
||||
}
|
||||
return dep
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectDankCalendar() deps.Dependency {
|
||||
dep := a.BaseDistribution.detectDankCalendar()
|
||||
dep.CanToggle = true
|
||||
if a.packageInstalled("dankcalendar-git") {
|
||||
dep.Variant = deps.VariantGit
|
||||
}
|
||||
return dep
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
||||
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
|
||||
}
|
||||
@@ -152,13 +132,7 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
||||
installed := a.packageInstalled("greetd-dms-greeter-git") || a.packageInstalled("greetd-dms-greeter-bin")
|
||||
dep := a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", installed)
|
||||
dep.CanToggle = true
|
||||
if a.packageInstalled("greetd-dms-greeter-git") {
|
||||
dep.Variant = deps.VariantGit
|
||||
}
|
||||
return dep
|
||||
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||
@@ -217,7 +191,7 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
||||
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": a.getDMSGreeterMapping(variants["dms-greeter"]),
|
||||
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
|
||||
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||
@@ -225,8 +199,6 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"danksearch": a.getDanksearchMapping(variants["danksearch"]),
|
||||
"dankcalendar": a.getDankCalendarMapping(variants["dankcalendar"]),
|
||||
}
|
||||
|
||||
switch wm {
|
||||
@@ -281,27 +253,6 @@ func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) Packag
|
||||
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getDanksearchMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dsearch-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
return PackageMapping{Name: "dsearch-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getDankCalendarMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dankcalendar-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
return PackageMapping{Name: "dankcalendar-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getDMSGreeterMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
return PackageMapping{Name: "greetd-dms-greeter-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceDMSGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||
|
||||
@@ -107,14 +107,6 @@ func (b *BaseDistribution) detectDgop() deps.Dependency {
|
||||
return b.detectCommand("dgop", "Desktop portal management tool")
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDanksearch() deps.Dependency {
|
||||
return b.detectOptionalPackage("danksearch", "File indexing and search service", b.commandExists("dsearch") || b.commandExists("danksearch"))
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDankCalendar() deps.Dependency {
|
||||
return b.detectOptionalPackage("dankcalendar", "Calendar application", b.commandExists("dcal") || b.commandExists("dankcalendar"))
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||
|
||||
|
||||
@@ -71,8 +71,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
|
||||
dependencies = append(dependencies, d.detectMatugen())
|
||||
dependencies = append(dependencies, d.detectDgop())
|
||||
dependencies = append(dependencies, d.detectDanksearch())
|
||||
dependencies = append(dependencies, d.detectDankCalendar())
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
@@ -137,8 +135,6 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
}
|
||||
|
||||
if wm == deps.WindowManagerNiri {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// SetupDsearchService enables the dsearch.service user unit. Enablement failures
|
||||
// are returned for the caller to surface as a non-fatal warning.
|
||||
func SetupDsearchService(ctx context.Context, logf func(string)) error {
|
||||
if logf == nil {
|
||||
logf = func(string) {}
|
||||
}
|
||||
|
||||
if err := runSystemctlUser(ctx, "daemon-reload"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := runSystemctlUser(ctx, "enable", "--now", "dsearch.service"); err != nil {
|
||||
return err
|
||||
}
|
||||
logf("Enabled dsearch.service")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSystemctlUser(ctx context.Context, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, "systemctl", append([]string{"--user"}, args...)...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl --user %v failed: %w: %s", args, err, string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -104,8 +104,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
|
||||
dependencies = append(dependencies, f.detectMatugen())
|
||||
dependencies = append(dependencies, f.detectDgop())
|
||||
dependencies = append(dependencies, f.detectDanksearch())
|
||||
dependencies = append(dependencies, f.detectDankCalendar())
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
@@ -140,8 +138,6 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
|
||||
@@ -113,7 +113,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
|
||||
dependencies = append(dependencies, g.detectMatugen())
|
||||
dependencies = append(dependencies, g.detectDgop())
|
||||
dependencies = append(dependencies, g.detectDanksearch())
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
@@ -172,7 +171,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
"dms (DankMaterialShell)": g.getDmsMapping(),
|
||||
"dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
"danksearch": {Name: "gui-apps/danksearch", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
|
||||
@@ -17,7 +17,6 @@ const (
|
||||
FamilyDebian DistroFamily = "debian"
|
||||
FamilyNix DistroFamily = "nix"
|
||||
FamilyGentoo DistroFamily = "gentoo"
|
||||
FamilyVoid DistroFamily = "void"
|
||||
)
|
||||
|
||||
// PackageManagerType defines the package manager a distro uses
|
||||
@@ -30,7 +29,6 @@ const (
|
||||
PackageManagerZypper PackageManagerType = "zypper"
|
||||
PackageManagerNix PackageManagerType = "nix"
|
||||
PackageManagerPortage PackageManagerType = "portage"
|
||||
PackageManagerXBPS PackageManagerType = "xbps"
|
||||
)
|
||||
|
||||
// RepositoryType defines the type of repository for a package
|
||||
@@ -44,7 +42,6 @@ const (
|
||||
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
||||
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
|
||||
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||
)
|
||||
|
||||
|
||||
@@ -91,8 +91,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
||||
|
||||
dependencies = append(dependencies, o.detectMatugen())
|
||||
dependencies = append(dependencies, o.detectDgop())
|
||||
dependencies = append(dependencies, o.detectDanksearch())
|
||||
dependencies = append(dependencies, o.detectDankCalendar())
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
@@ -131,8 +129,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
|
||||
@@ -80,8 +80,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
|
||||
dependencies = append(dependencies, u.detectMatugen())
|
||||
dependencies = append(dependencies, u.detectDgop())
|
||||
dependencies = append(dependencies, u.detectDanksearch())
|
||||
dependencies = append(dependencies, u.detectDankCalendar())
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
@@ -126,8 +124,6 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
|
||||
@@ -1,534 +0,0 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
)
|
||||
|
||||
const (
|
||||
VoidDMSRepo = "https://avengemedia.github.io/DankMaterialShell/current"
|
||||
VoidDankLinuxRepo = "https://avengemedia.github.io/DankLinux/current"
|
||||
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
|
||||
|
||||
voidRunitSvDir = "/etc/sv"
|
||||
voidRunitServiceDir = "/var/service"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("void", "#478061", FamilyVoid, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewVoidDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type VoidDistribution struct {
|
||||
*BaseDistribution
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewVoidDistribution(config DistroConfig, logChan chan<- string) *VoidDistribution {
|
||||
return &VoidDistribution{
|
||||
BaseDistribution: NewBaseDistribution(logChan),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) GetID() string {
|
||||
return v.config.ID
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) GetColorHex() string {
|
||||
return v.config.ColorHex
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) GetFamily() DistroFamily {
|
||||
return v.config.Family
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerXBPS
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return v.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
dependencies = append(dependencies, v.detectDMS())
|
||||
dependencies = append(dependencies, v.detectSpecificTerminal(terminal))
|
||||
dependencies = append(dependencies, v.detectGit())
|
||||
dependencies = append(dependencies, v.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, v.detectQuickshell())
|
||||
dependencies = append(dependencies, v.detectDMSGreeter())
|
||||
dependencies = append(dependencies, v.detectXDGPortal())
|
||||
dependencies = append(dependencies, v.detectAccountsService())
|
||||
dependencies = append(dependencies, v.detectDBus())
|
||||
dependencies = append(dependencies, v.detectElogind())
|
||||
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, v.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
if wm == deps.WindowManagerNiri || wm == deps.WindowManagerMango {
|
||||
dependencies = append(dependencies, v.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, v.detectMatugen())
|
||||
dependencies = append(dependencies, v.detectDgop())
|
||||
dependencies = append(dependencies, v.detectDanksearch())
|
||||
dependencies = append(dependencies, v.detectDankCalendar())
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) detectDMS() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
version := ""
|
||||
variant := deps.VariantStable
|
||||
|
||||
if v.packageInstalled("dms-git") {
|
||||
status = deps.StatusInstalled
|
||||
version = v.packageVersion("dms-git")
|
||||
variant = deps.VariantGit
|
||||
} else if v.packageInstalled("dms") {
|
||||
status = deps.StatusInstalled
|
||||
version = v.packageVersion("dms")
|
||||
} else if v.commandExists("dms") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "dms (DankMaterialShell)",
|
||||
Status: status,
|
||||
Version: version,
|
||||
Description: "Desktop Management System package",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) detectQuickshell() deps.Dependency {
|
||||
dep := v.BaseDistribution.detectQuickshell()
|
||||
dep.CanToggle = false
|
||||
return dep
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) detectXDGPortal() deps.Dependency {
|
||||
return v.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", v.packageInstalled("xdg-desktop-portal-gtk"))
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return v.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", v.packageInstalled("dms-greeter"))
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) detectAccountsService() deps.Dependency {
|
||||
return v.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", v.packageInstalled("accountsservice"))
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) detectDBus() deps.Dependency {
|
||||
return v.detectPackage("dbus", "D-Bus system and session message bus", v.packageInstalled("dbus"))
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) detectElogind() deps.Dependency {
|
||||
return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl"))
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite"))
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) packageInstalled(pkg string) bool {
|
||||
return exec.Command("xbps-query", pkg).Run() == nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) packageVersion(pkg string) string {
|
||||
output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return v.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"dbus": {Name: "dbus", Repository: RepoTypeSystem},
|
||||
"elogind": {Name: "elogind", Repository: RepoTypeSystem},
|
||||
|
||||
"quickshell": {Name: "quickshell", Repository: RepoTypeSystem},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
|
||||
"dms (DankMaterialShell)": v.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
|
||||
"dankcalendar": {Name: "dankcalendar", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
|
||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
|
||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerMango:
|
||||
packages["mango"] = PackageMapping{Name: "mangowc", Repository: RepoTypeSystem}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantStable {
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
|
||||
}
|
||||
return PackageMapping{Name: "dms-git", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Checking XBPS...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Checking for xbps-install",
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("xbps-install"); err != nil {
|
||||
return fmt.Errorf("xbps-install not found; Void Linux package tools are required: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
arch, err := v.xbpsArch(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect XBPS architecture for Hyprland repository selection: %w", err)
|
||||
}
|
||||
if arch != "x86_64" {
|
||||
return fmt.Errorf("hyprland on Void Linux is installed from %s, which currently provides x86_64 packages only (detected %s)", VoidHyprlandRepo, arch)
|
||||
}
|
||||
}
|
||||
|
||||
if err := v.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, xbpsPkgs := v.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||
|
||||
if len(xbpsPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.15,
|
||||
Step: "Enabling DMS XBPS repositories...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: "Setting up custom XBPS repositories for DMS packages",
|
||||
}
|
||||
if err := v.enableXBPSRepos(ctx, xbpsPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to enable XBPS repositories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
allPkgs := v.uniquePackageNames(systemPkgs, v.extractPackageNames(xbpsPkgs))
|
||||
if len(allPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d XBPS packages...", len(allPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing XBPS packages: %s", strings.Join(allPkgs, ", ")),
|
||||
}
|
||||
if err := v.installXBPSPackages(ctx, allPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install XBPS packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
v.log("Void Linux detected; DMS environment and autostart will be configured in the compositor config instead of systemd")
|
||||
if err := v.ensureSessionServices(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to enable Void session services: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if !v.isRunitSystem() {
|
||||
v.log("Void runit service directory not detected; skipping dbus/elogind service enablement")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, service := range []string{"dbus", "elogind"} {
|
||||
if !v.runitServiceInstalled(service) {
|
||||
v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service))
|
||||
continue
|
||||
}
|
||||
if v.runitServiceEnabled(service) {
|
||||
v.log(fmt.Sprintf("Void runit service %s already enabled", service))
|
||||
continue
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.92,
|
||||
Step: fmt.Sprintf("Enabling %s runit service...", service),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)),
|
||||
LogOutput: fmt.Sprintf("Enabling Void runit service: %s", service),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)))
|
||||
if err := v.runWithProgress(cmd, progressChan, PhaseConfiguration, 0.92, 0.95); err != nil {
|
||||
return fmt.Errorf("failed to enable %s runit service: %w", service, err)
|
||||
}
|
||||
v.log(fmt.Sprintf("✓ Enabled %s runit service", service))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) isRunitSystem() bool {
|
||||
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
|
||||
return true
|
||||
}
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
return false
|
||||
}
|
||||
if fi, err := os.Stat(voidRunitServiceDir); err == nil && fi.IsDir() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) runitServiceInstalled(name string) bool {
|
||||
fi, err := os.Stat(filepath.Join(voidRunitSvDir, name))
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) runitServiceEnabled(name string) bool {
|
||||
_, err := os.Lstat(filepath.Join(voidRunitServiceDir, name))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping) {
|
||||
systemPkgs := []string{}
|
||||
xbpsPkgs := []PackageMapping{}
|
||||
|
||||
variantMap := make(map[string]deps.PackageVariant)
|
||||
for _, dep := range dependencies {
|
||||
variantMap[dep.Name] = dep.Variant
|
||||
}
|
||||
|
||||
packageMap := v.GetPackageMappingWithVariants(wm, variantMap)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
if disabledFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
v.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeXBPS:
|
||||
xbpsPkgs = append(xbpsPkgs, pkgInfo)
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, xbpsPkgs
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) enableXBPSRepos(ctx context.Context, xbpsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
enabledRepoURLs := []string{}
|
||||
|
||||
for _, pkg := range xbpsPkgs {
|
||||
if pkg.RepoURL == "" || enabledRepos[pkg.RepoURL] {
|
||||
continue
|
||||
}
|
||||
|
||||
repoName := v.xbpsRepoName(pkg.RepoURL)
|
||||
confPath := filepath.Join("/etc/xbps.d", repoName+".conf")
|
||||
repoLine := fmt.Sprintf("repository=%s", pkg.RepoURL)
|
||||
repoFileContent := repoLine + "\n"
|
||||
|
||||
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
|
||||
v.log(fmt.Sprintf("XBPS repo %s already configured, skipping", pkg.RepoURL))
|
||||
enabledRepos[pkg.RepoURL] = true
|
||||
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
|
||||
continue
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.18,
|
||||
Step: fmt.Sprintf("Adding XBPS repo %s...", repoName),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("echo 'repository=%s' | sudo tee %s", pkg.RepoURL, confPath),
|
||||
LogOutput: fmt.Sprintf("Adding XBPS repository: %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/xbps.d")
|
||||
if err := v.runWithProgress(mkdirCmd, progressChan, PhaseSystemPackages, 0.18, 0.19); err != nil {
|
||||
return fmt.Errorf("failed to create /etc/xbps.d: %w", err)
|
||||
}
|
||||
|
||||
writeCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c 'printf \"%%s\\n\" %q > %s'", repoLine, confPath))
|
||||
if err := v.runWithProgress(writeCmd, progressChan, PhaseSystemPackages, 0.19, 0.22); err != nil {
|
||||
return fmt.Errorf("failed to add XBPS repo %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
|
||||
enabledRepos[pkg.RepoURL] = true
|
||||
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
|
||||
}
|
||||
|
||||
if len(enabledRepos) > 0 {
|
||||
syncArgs := []string{"xbps-install", "-Sy", "-i"}
|
||||
for _, repoURL := range enabledRepoURLs {
|
||||
syncArgs = append(syncArgs, "--repository", repoURL)
|
||||
}
|
||||
syncCommand := strings.Join(syncArgs, " ")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.25,
|
||||
Step: "Synchronizing XBPS repositories...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo sh -c 'yes y | " + syncCommand + "'",
|
||||
LogOutput: "Synchronizing XBPS repository indexes",
|
||||
}
|
||||
|
||||
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | "+syncCommand+"'")
|
||||
if err := v.runWithProgress(syncCmd, progressChan, PhaseSystemPackages, 0.25, 0.30); err != nil {
|
||||
return fmt.Errorf("failed to synchronize XBPS repositories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) xbpsRepoName(repoURL string) string {
|
||||
switch repoURL {
|
||||
case VoidDMSRepo:
|
||||
return "dms"
|
||||
case VoidDankLinuxRepo:
|
||||
return "danklinux"
|
||||
case VoidHyprlandRepo:
|
||||
return "hyprland"
|
||||
default:
|
||||
name := strings.TrimPrefix(repoURL, "https://")
|
||||
name = strings.TrimPrefix(name, "http://")
|
||||
name = strings.NewReplacer("/", "-", ".", "-").Replace(name)
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) xbpsArch(ctx context.Context) (string, error) {
|
||||
output, err := exec.CommandContext(ctx, "xbps-uhelper", "arch").Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) installXBPSPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
args := append([]string{"xbps-install", "-Sy"}, packages...)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing XBPS packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return v.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.85)
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
names[i] = pkg.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) uniquePackageNames(groups ...[]string) []string {
|
||||
seen := make(map[string]bool)
|
||||
var unique []string
|
||||
for _, group := range groups {
|
||||
for _, pkg := range group {
|
||||
if pkg == "" || seen[pkg] {
|
||||
continue
|
||||
}
|
||||
seen[pkg] = true
|
||||
unique = append(unique, pkg)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
@@ -30,11 +30,6 @@ const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
||||
|
||||
const GreeterCacheDir = "/var/cache/dms-greeter"
|
||||
|
||||
const (
|
||||
runitSvDir = "/etc/sv"
|
||||
runitServiceDir = "/var/service"
|
||||
)
|
||||
|
||||
func DetectDMSPath() (string, error) {
|
||||
return config.LocateDMSConfig()
|
||||
}
|
||||
@@ -46,96 +41,6 @@ func IsNixOS() bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func IsVoidLinux() bool {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
return exists && config.Family == distros.FamilyVoid
|
||||
}
|
||||
|
||||
func isRunit() bool {
|
||||
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
|
||||
return true
|
||||
}
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
return false
|
||||
}
|
||||
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func runitServiceInstalled(name string) bool {
|
||||
fi, err := os.Stat(filepath.Join(runitSvDir, name))
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func runitServiceEnabled(name string) bool {
|
||||
_, err := os.Lstat(filepath.Join(runitServiceDir, name))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func enableRunitService(name, sudoPassword string) error {
|
||||
if !runitServiceInstalled(name) {
|
||||
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
|
||||
}
|
||||
if runitServiceEnabled(name) {
|
||||
return nil
|
||||
}
|
||||
return privesc.Run(context.Background(), sudoPassword, "ln", "-sf",
|
||||
filepath.Join(runitSvDir, name), filepath.Join(runitServiceDir, name))
|
||||
}
|
||||
|
||||
func disableRunitService(name, sudoPassword string) error {
|
||||
if !runitServiceEnabled(name) {
|
||||
return nil
|
||||
}
|
||||
return privesc.Run(context.Background(), sudoPassword, "rm", "-f",
|
||||
filepath.Join(runitServiceDir, name))
|
||||
}
|
||||
|
||||
func ensureRunitSeat(greeterUser, sudoPassword string, logFunc func(string)) {
|
||||
if runitServiceInstalled("seatd") {
|
||||
if err := enableRunitService("seatd", sudoPassword); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ could not enable seatd: %v", err))
|
||||
} else {
|
||||
logFunc("✓ seatd enabled")
|
||||
}
|
||||
} else {
|
||||
logFunc("⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ could not add %s to seat groups: %v", greeterUser, err))
|
||||
} else {
|
||||
logFunc(fmt.Sprintf("✓ %s added to seat groups (_seatd, video, input)", greeterUser))
|
||||
}
|
||||
}
|
||||
|
||||
func ensureGreetdPamRundir(sudoPassword string, logFunc func(string)) {
|
||||
const pamPath = "/etc/pam.d/greetd"
|
||||
data, err := os.ReadFile(pamPath)
|
||||
if err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ could not read %s: %v", pamPath, err))
|
||||
return
|
||||
}
|
||||
if strings.Contains(string(data), "pam_rundir") {
|
||||
logFunc("✓ pam_rundir already present in greetd PAM")
|
||||
return
|
||||
}
|
||||
|
||||
line := "session optional pam_rundir.so"
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c",
|
||||
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ could not add pam_rundir to %s: %v", pamPath, err))
|
||||
return
|
||||
}
|
||||
logFunc("✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
|
||||
}
|
||||
|
||||
func DetectGreeterGroup() string {
|
||||
data, err := os.ReadFile("/etc/group")
|
||||
if err != nil {
|
||||
@@ -861,8 +766,6 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
|
||||
case distros.FamilyGentoo:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
|
||||
case distros.FamilyVoid:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy greetd")
|
||||
case distros.FamilyNix:
|
||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||
default:
|
||||
@@ -989,14 +892,6 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
||||
}
|
||||
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper)
|
||||
installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git")
|
||||
case distros.FamilyVoid:
|
||||
failHint = "⚠ dms-greeter install failed. Add the DMS XBPS repo manually:\necho 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf\nsudo xbps-install -Sy dms-greeter"
|
||||
logFunc("Adding DMS XBPS repository...")
|
||||
if err := ensureVoidXBPSRepo(ctx, sudoPassword, "dms", distros.VoidDMSRepo); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Failed to add DMS XBPS repository: %v", err))
|
||||
}
|
||||
privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | xbps-install -Sy -i --repository "+distros.VoidDMSRepo+"'").Run()
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy dms-greeter")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -1014,20 +909,6 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func ensureVoidXBPSRepo(ctx context.Context, sudoPassword, name, repoURL string) error {
|
||||
confPath := filepath.Join("/etc/xbps.d", name+".conf")
|
||||
repoLine := fmt.Sprintf("repository=%s", repoURL)
|
||||
repoFileContent := repoLine + "\n"
|
||||
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
|
||||
return nil
|
||||
}
|
||||
if err := privesc.Run(ctx, sudoPassword, "mkdir", "-p", "/etc/xbps.d"); err != nil {
|
||||
return err
|
||||
}
|
||||
return privesc.Run(ctx, sudoPassword, "sh", "-c",
|
||||
fmt.Sprintf("printf '%%s\\n' %q > %s", repoLine, confPath))
|
||||
}
|
||||
|
||||
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
||||
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||
if IsGreeterPackaged() {
|
||||
@@ -2394,19 +2275,6 @@ func checkSystemdEnabled(service string) (string, error) {
|
||||
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
|
||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||
for _, dm := range conflictingDMs {
|
||||
if isRunit() {
|
||||
if !runitServiceEnabled(dm) {
|
||||
continue
|
||||
}
|
||||
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
||||
if err := disableRunitService(dm, sudoPassword); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
||||
} else {
|
||||
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
state, err := checkSystemdEnabled(dm)
|
||||
if err != nil || state == "" || state == "not-found" {
|
||||
continue
|
||||
@@ -2426,19 +2294,6 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
|
||||
|
||||
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
|
||||
func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
||||
if isRunit() {
|
||||
if !runitServiceInstalled("greetd") {
|
||||
return fmt.Errorf("greetd service not found in %s; ensure greetd is installed", runitSvDir)
|
||||
}
|
||||
ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc)
|
||||
ensureGreetdPamRundir(sudoPassword, logFunc)
|
||||
if err := enableRunitService("greetd", sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ greetd enabled (%s)", runitServiceDir))
|
||||
return nil
|
||||
}
|
||||
|
||||
state, err := checkSystemdEnabled("greetd")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check greetd state: %w", err)
|
||||
@@ -2462,11 +2317,6 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
||||
}
|
||||
|
||||
func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
|
||||
if isRunit() {
|
||||
logFunc("✓ runit detected; no graphical target is needed")
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("systemctl", "get-default")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
||||
@@ -41,8 +41,6 @@ type Config struct {
|
||||
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
|
||||
ReplaceConfigsAll bool // deploy/replace all configurations
|
||||
Yes bool
|
||||
DankSearch bool // install danksearch and enable its user service
|
||||
DankCalendar bool // install dankcalendar
|
||||
}
|
||||
|
||||
// Runner orchestrates unattended (headless) installation.
|
||||
@@ -216,11 +214,6 @@ func (r *Runner) Run() error {
|
||||
return fmt.Errorf("package installation failed: %w", err)
|
||||
}
|
||||
|
||||
useSystemd := true
|
||||
if distroConfig, exists := distros.Registry[osInfo.Distribution.ID]; exists && distroConfig.Family == distros.FamilyVoid {
|
||||
useSystemd = false
|
||||
}
|
||||
|
||||
// 9. Greeter setup (if dms-greeter was included)
|
||||
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
|
||||
compositorName := "niri"
|
||||
@@ -238,32 +231,18 @@ func (r *Runner) Run() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 9b. danksearch service setup (if danksearch was included)
|
||||
if useSystemd && !disabledItems["danksearch"] && r.depExists(dependencies, "danksearch") {
|
||||
fmt.Fprintln(os.Stdout, "Enabling danksearch service...")
|
||||
logFunc := func(line string) {
|
||||
r.log(line)
|
||||
fmt.Fprintf(os.Stdout, " danksearch: %s\n", line)
|
||||
}
|
||||
if err := distros.SetupDsearchService(context.Background(), logFunc); err != nil {
|
||||
// Non-fatal, matching greeter behavior
|
||||
fmt.Fprintf(os.Stderr, "Warning: danksearch service setup issue (non-fatal): %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Deploy configurations
|
||||
fmt.Fprintln(os.Stdout, "Deploying configurations...")
|
||||
r.log("Starting configuration deployment")
|
||||
|
||||
deployer := config.NewConfigDeployer(r.logChan)
|
||||
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(
|
||||
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
|
||||
context.Background(),
|
||||
wm,
|
||||
terminal,
|
||||
dependencies,
|
||||
replaceConfigs,
|
||||
reinstallItems,
|
||||
useSystemd,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configuration deployment failed: %w", err)
|
||||
@@ -288,31 +267,19 @@ func (r *Runner) Run() error {
|
||||
}
|
||||
|
||||
// buildDisabledItems computes the set of dependencies that should be skipped
|
||||
// during installation. Optional components are opt-in (disabled by default),
|
||||
// then re-enabled by the dedicated flags and --include-deps.
|
||||
// during installation, applying the --include-deps and --exclude-deps filters.
|
||||
// dms-greeter is disabled by default (opt-in), matching TUI behavior.
|
||||
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
|
||||
disabledItems := make(map[string]bool)
|
||||
|
||||
// dms-greeter is opt-in (disabled by default), matching TUI behavior
|
||||
for i := range dependencies {
|
||||
if !dependencies[i].Required {
|
||||
disabledItems[dependencies[i].Name] = true
|
||||
if dependencies[i].Name == "dms-greeter" {
|
||||
disabledItems["dms-greeter"] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated flags resolve before include/exclude
|
||||
if r.cfg.DankSearch {
|
||||
if !r.depExists(dependencies, "danksearch") {
|
||||
return nil, fmt.Errorf("--danksearch: not available on this distribution")
|
||||
}
|
||||
delete(disabledItems, "danksearch")
|
||||
}
|
||||
if r.cfg.DankCalendar {
|
||||
if !r.depExists(dependencies, "dankcalendar") {
|
||||
return nil, fmt.Errorf("--dankcalendar: not available on this distribution")
|
||||
}
|
||||
delete(disabledItems, "dankcalendar")
|
||||
}
|
||||
|
||||
// Process --include-deps (enable items that are disabled by default)
|
||||
for _, name := range r.cfg.IncludeDeps {
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
@@ -342,21 +342,17 @@ func TestConfigReplaceConfigsStoredCorrectly(t *testing.T) {
|
||||
|
||||
func TestBuildDisabledItems(t *testing.T) {
|
||||
dependencies := []deps.Dependency{
|
||||
{Name: "niri", Status: deps.StatusInstalled, Required: true},
|
||||
{Name: "ghostty", Status: deps.StatusMissing, Required: true},
|
||||
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled, Required: true},
|
||||
{Name: "niri", Status: deps.StatusInstalled},
|
||||
{Name: "ghostty", Status: deps.StatusMissing},
|
||||
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
|
||||
{Name: "dms-greeter", Status: deps.StatusMissing},
|
||||
{Name: "danksearch", Status: deps.StatusMissing},
|
||||
{Name: "dankcalendar", Status: deps.StatusMissing},
|
||||
{Name: "waybar", Status: deps.StatusMissing, Required: true},
|
||||
{Name: "waybar", Status: deps.StatusMissing},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
includeDeps []string
|
||||
excludeDeps []string
|
||||
dankSearch bool
|
||||
dankCalendar bool
|
||||
deps []deps.Dependency // nil means use the shared fixture
|
||||
wantErr bool
|
||||
errContains string // substring expected in error message
|
||||
@@ -364,20 +360,19 @@ func TestBuildDisabledItems(t *testing.T) {
|
||||
wantEnabled []string // dep names that should NOT be in disabledItems (extra check)
|
||||
}{
|
||||
{
|
||||
name: "no flags set, optional deps disabled by default",
|
||||
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar"},
|
||||
name: "no flags set, dms-greeter disabled by default",
|
||||
wantDisabled: []string{"dms-greeter"},
|
||||
wantEnabled: []string{"niri", "ghostty", "waybar"},
|
||||
},
|
||||
{
|
||||
name: "include dms-greeter enables it",
|
||||
includeDeps: []string{"dms-greeter"},
|
||||
wantEnabled: []string{"dms-greeter"},
|
||||
wantDisabled: []string{"danksearch", "dankcalendar"},
|
||||
name: "include dms-greeter enables it",
|
||||
includeDeps: []string{"dms-greeter"},
|
||||
wantEnabled: []string{"dms-greeter"},
|
||||
},
|
||||
{
|
||||
name: "exclude a regular dep",
|
||||
excludeDeps: []string{"waybar"},
|
||||
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar", "waybar"},
|
||||
wantDisabled: []string{"dms-greeter", "waybar"},
|
||||
},
|
||||
{
|
||||
name: "include unknown dep returns error",
|
||||
@@ -404,53 +399,24 @@ func TestBuildDisabledItems(t *testing.T) {
|
||||
wantDisabled: []string{"dms-greeter"},
|
||||
},
|
||||
{
|
||||
name: "whitespace entries are skipped",
|
||||
includeDeps: []string{" ", "dms-greeter"},
|
||||
wantEnabled: []string{"dms-greeter"},
|
||||
wantDisabled: []string{"danksearch", "dankcalendar"},
|
||||
name: "whitespace entries are skipped",
|
||||
includeDeps: []string{" ", "dms-greeter"},
|
||||
wantEnabled: []string{"dms-greeter"},
|
||||
},
|
||||
{
|
||||
name: "no optional deps present, nothing disabled by default",
|
||||
name: "no dms-greeter in deps, nothing disabled by default",
|
||||
deps: []deps.Dependency{
|
||||
{Name: "niri", Status: deps.StatusInstalled, Required: true},
|
||||
{Name: "niri", Status: deps.StatusInstalled},
|
||||
},
|
||||
wantEnabled: []string{"niri"},
|
||||
},
|
||||
{
|
||||
name: "danksearch flag enables it",
|
||||
dankSearch: true,
|
||||
wantEnabled: []string{"danksearch"},
|
||||
wantDisabled: []string{"dms-greeter", "dankcalendar"},
|
||||
},
|
||||
{
|
||||
name: "dankcalendar flag enables it",
|
||||
dankCalendar: true,
|
||||
wantEnabled: []string{"dankcalendar"},
|
||||
wantDisabled: []string{"dms-greeter", "danksearch"},
|
||||
},
|
||||
{
|
||||
name: "danksearch flag when unavailable errors",
|
||||
dankSearch: true,
|
||||
deps: []deps.Dependency{{Name: "niri", Status: deps.StatusInstalled, Required: true}},
|
||||
wantErr: true,
|
||||
errContains: "--danksearch",
|
||||
},
|
||||
{
|
||||
name: "dankcalendar flag when unavailable errors",
|
||||
dankCalendar: true,
|
||||
deps: []deps.Dependency{{Name: "niri", Status: deps.StatusInstalled, Required: true}},
|
||||
wantErr: true,
|
||||
errContains: "--dankcalendar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := NewRunner(Config{
|
||||
IncludeDeps: tt.includeDeps,
|
||||
ExcludeDeps: tt.excludeDeps,
|
||||
DankSearch: tt.dankSearch,
|
||||
DankCalendar: tt.dankCalendar,
|
||||
IncludeDeps: tt.includeDeps,
|
||||
ExcludeDeps: tt.excludeDeps,
|
||||
})
|
||||
d := tt.deps
|
||||
if d == nil {
|
||||
|
||||
@@ -1154,7 +1154,7 @@ func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := expandLuaConfigLines(strings.Split(string(data), "\n"))
|
||||
lines := strings.Split(string(data), "\n")
|
||||
parser := NewHyprlandParser("")
|
||||
pendingUnbinds := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Lua configs can express binds dynamically: variables (mainMod .. " + C"),
|
||||
// tostring() calls, and numeric for loops (workspace binds). This resolves such
|
||||
// expressions to literal key combos so the static bind parser can read them.
|
||||
|
||||
const luaMaxLoopIterations = 1000
|
||||
|
||||
var (
|
||||
luaAssignRE = regexp.MustCompile(`^(?:local\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$`)
|
||||
luaForRE = regexp.MustCompile(`^for\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(-?\d+)\s*,\s*(-?\d+)\s*(?:,\s*(-?\d+)\s*)?do\b(.*)$`)
|
||||
luaTostringRE = regexp.MustCompile(`to(?:string|number)\s*\(\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|-?\d+(?:\.\d+)?)\s*\)`)
|
||||
luaBlockOpenRE = regexp.MustCompile(`\b(?:function|for|while|if)\b`)
|
||||
luaBlockCloseRE = regexp.MustCompile(`\bend\b`)
|
||||
luaNumberRE = regexp.MustCompile(`^-?\d+(?:\.\d+)?$`)
|
||||
)
|
||||
|
||||
type luaVarEnv map[string]string
|
||||
|
||||
type luaForHeader struct {
|
||||
varName string
|
||||
start int
|
||||
stop int
|
||||
step int
|
||||
inline string
|
||||
}
|
||||
|
||||
func expandLuaConfigLines(lines []string) []string {
|
||||
return expandLuaBlock(lines, luaVarEnv{})
|
||||
}
|
||||
|
||||
func expandLuaBlock(lines []string, env luaVarEnv) []string {
|
||||
out := make([]string, 0, len(lines))
|
||||
for i := 0; i < len(lines); i++ {
|
||||
code := strings.TrimSpace(luaStripLineComment(lines[i]))
|
||||
|
||||
if name, value, ok := parseLuaStringAssignment(code, env); ok {
|
||||
env[name] = value
|
||||
out = append(out, lines[i])
|
||||
continue
|
||||
}
|
||||
|
||||
header, ok := parseLuaForHeader(code)
|
||||
if !ok {
|
||||
out = append(out, resolveLuaDynamicLine(lines[i], env))
|
||||
continue
|
||||
}
|
||||
|
||||
body, consumed, complete := collectLuaForBody(header, lines, i)
|
||||
if !complete {
|
||||
out = append(out, resolveLuaDynamicLine(lines[i], env))
|
||||
continue
|
||||
}
|
||||
out = append(out, expandLuaForLoop(header, body, env)...)
|
||||
i = consumed
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseLuaStringAssignment(code string, env luaVarEnv) (name, value string, ok bool) {
|
||||
m := luaAssignRE.FindStringSubmatch(code)
|
||||
if m == nil {
|
||||
return "", "", false
|
||||
}
|
||||
value, ok = evalLuaConcat(m[2], env)
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
return m[1], value, true
|
||||
}
|
||||
|
||||
func parseLuaForHeader(code string) (luaForHeader, bool) {
|
||||
m := luaForRE.FindStringSubmatch(code)
|
||||
if m == nil {
|
||||
return luaForHeader{}, false
|
||||
}
|
||||
start, _ := strconv.Atoi(m[2])
|
||||
stop, _ := strconv.Atoi(m[3])
|
||||
step := 1
|
||||
if m[4] != "" {
|
||||
step, _ = strconv.Atoi(m[4])
|
||||
}
|
||||
if step == 0 {
|
||||
return luaForHeader{}, false
|
||||
}
|
||||
return luaForHeader{varName: m[1], start: start, stop: stop, step: step, inline: strings.TrimSpace(m[5])}, true
|
||||
}
|
||||
|
||||
func collectLuaForBody(header luaForHeader, lines []string, headerIdx int) (body []string, consumed int, complete bool) {
|
||||
depth := 1
|
||||
if header.inline != "" {
|
||||
delta := luaBlockDelta(header.inline)
|
||||
if depth+delta <= 0 {
|
||||
if stmt := strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(header.inline), "end")); stmt != "" {
|
||||
body = append(body, stmt)
|
||||
}
|
||||
return body, headerIdx, true
|
||||
}
|
||||
depth += delta
|
||||
body = append(body, header.inline)
|
||||
}
|
||||
for j := headerIdx + 1; j < len(lines); j++ {
|
||||
delta := luaBlockDelta(lines[j])
|
||||
if depth+delta <= 0 {
|
||||
return body, j, true
|
||||
}
|
||||
depth += delta
|
||||
body = append(body, lines[j])
|
||||
}
|
||||
return nil, headerIdx, false
|
||||
}
|
||||
|
||||
func expandLuaForLoop(header luaForHeader, body []string, env luaVarEnv) []string {
|
||||
var out []string
|
||||
inRange := func(v int) bool {
|
||||
if header.step > 0 {
|
||||
return v <= header.stop
|
||||
}
|
||||
return v >= header.stop
|
||||
}
|
||||
count := 0
|
||||
for v := header.start; inRange(v); v += header.step {
|
||||
if count++; count > luaMaxLoopIterations {
|
||||
break
|
||||
}
|
||||
value := strconv.Itoa(v)
|
||||
iterLines := make([]string, len(body))
|
||||
for k, bl := range body {
|
||||
iterLines[k] = substituteLuaIdent(bl, header.varName, value)
|
||||
}
|
||||
out = append(out, expandLuaBlock(iterLines, cloneLuaEnv(env))...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func luaBlockDelta(line string) int {
|
||||
masked := luaMaskStrings(line)
|
||||
return len(luaBlockOpenRE.FindAllString(masked, -1)) - len(luaBlockCloseRE.FindAllString(masked, -1))
|
||||
}
|
||||
|
||||
func resolveLuaDynamicLine(line string, env luaVarEnv) string {
|
||||
if !strings.Contains(line, "hl.bind") && !strings.Contains(line, "hl.unbind") {
|
||||
return line
|
||||
}
|
||||
line = normalizeLuaToString(line)
|
||||
return rewriteLuaBindKeyArg(line, env)
|
||||
}
|
||||
|
||||
func normalizeLuaToString(line string) string {
|
||||
return luaTostringRE.ReplaceAllStringFunc(line, func(m string) string {
|
||||
inner := luaTostringRE.FindStringSubmatch(m)[1]
|
||||
if inner[0] == '"' || inner[0] == '\'' {
|
||||
return inner
|
||||
}
|
||||
return strconv.Quote(inner)
|
||||
})
|
||||
}
|
||||
|
||||
func rewriteLuaBindKeyArg(line string, env luaVarEnv) string {
|
||||
for _, fn := range []string{"hl.bind", "hl.unbind"} {
|
||||
idx := strings.Index(line, fn)
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
open := skipLuaWS(line, idx+len(fn))
|
||||
if open >= len(line) || line[open] != '(' {
|
||||
continue
|
||||
}
|
||||
argStart := skipLuaWS(line, open+1)
|
||||
expr, end, ok := parseLuaFirstArgExpr(line, argStart)
|
||||
if !ok || isLuaPlainStringArg(expr) {
|
||||
continue
|
||||
}
|
||||
value, ok := evalLuaConcat(expr, env)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return line[:argStart] + strconv.Quote(value) + line[end:]
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func evalLuaConcat(expr string, env luaVarEnv) (string, bool) {
|
||||
parts := splitLuaConcat(expr)
|
||||
var sb strings.Builder
|
||||
for _, part := range parts {
|
||||
value, ok := evalLuaOperand(part, env)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
sb.WriteString(value)
|
||||
}
|
||||
return sb.String(), true
|
||||
}
|
||||
|
||||
func evalLuaOperand(op string, env luaVarEnv) (string, bool) {
|
||||
op = strings.TrimSpace(op)
|
||||
if op == "" {
|
||||
return "", false
|
||||
}
|
||||
switch op[0] {
|
||||
case '"', '\'':
|
||||
s, next, ok := parseLuaStringLiteral(op, 0)
|
||||
return s, next == len(op) && ok
|
||||
}
|
||||
if luaNumberRE.MatchString(op) {
|
||||
return op, true
|
||||
}
|
||||
if inner, ok := luaUnwrapCall(op, "tostring"); ok {
|
||||
return evalLuaConcat(inner, env)
|
||||
}
|
||||
if inner, ok := luaUnwrapCall(op, "tonumber"); ok {
|
||||
return evalLuaConcat(inner, env)
|
||||
}
|
||||
if value, ok := env[op]; ok {
|
||||
return value, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func splitLuaConcat(expr string) []string {
|
||||
var parts []string
|
||||
parenDepth, braceDepth, bracketDepth := 0, 0, 0
|
||||
inStr := byte(0)
|
||||
esc := false
|
||||
start := 0
|
||||
for i := 0; i < len(expr); i++ {
|
||||
c := expr[i]
|
||||
if inStr != 0 {
|
||||
switch {
|
||||
case esc:
|
||||
esc = false
|
||||
case c == '\\' && inStr == '"':
|
||||
esc = true
|
||||
case c == inStr:
|
||||
inStr = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch c {
|
||||
case '"', '\'':
|
||||
inStr = c
|
||||
case '(':
|
||||
parenDepth++
|
||||
case ')':
|
||||
if parenDepth > 0 {
|
||||
parenDepth--
|
||||
}
|
||||
case '{':
|
||||
braceDepth++
|
||||
case '}':
|
||||
if braceDepth > 0 {
|
||||
braceDepth--
|
||||
}
|
||||
case '[':
|
||||
bracketDepth++
|
||||
case ']':
|
||||
if bracketDepth > 0 {
|
||||
bracketDepth--
|
||||
}
|
||||
case '.':
|
||||
if parenDepth == 0 && braceDepth == 0 && bracketDepth == 0 && i+1 < len(expr) && expr[i+1] == '.' {
|
||||
parts = append(parts, expr[start:i])
|
||||
i++
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return append(parts, expr[start:])
|
||||
}
|
||||
|
||||
func substituteLuaIdent(line, name, value string) string {
|
||||
if !strings.Contains(line, name) {
|
||||
return line
|
||||
}
|
||||
var sb strings.Builder
|
||||
inStr := byte(0)
|
||||
esc := false
|
||||
for i := 0; i < len(line); {
|
||||
c := line[i]
|
||||
if inStr != 0 {
|
||||
sb.WriteByte(c)
|
||||
switch {
|
||||
case esc:
|
||||
esc = false
|
||||
case c == '\\' && inStr == '"':
|
||||
esc = true
|
||||
case c == inStr:
|
||||
inStr = 0
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if c == '"' || c == '\'' {
|
||||
inStr = c
|
||||
sb.WriteByte(c)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if isLuaIdentStart(c) {
|
||||
j := i + 1
|
||||
for j < len(line) && isLuaIdentByte(line[j]) {
|
||||
j++
|
||||
}
|
||||
word := line[i:j]
|
||||
if word == name && (i == 0 || line[i-1] != '.') {
|
||||
sb.WriteString(value)
|
||||
} else {
|
||||
sb.WriteString(word)
|
||||
}
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
sb.WriteByte(c)
|
||||
i++
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func luaMaskStrings(line string) string {
|
||||
b := []byte(line)
|
||||
inStr := byte(0)
|
||||
esc := false
|
||||
for i := 0; i < len(b); i++ {
|
||||
c := b[i]
|
||||
if inStr != 0 {
|
||||
wasEnd := !esc && c == inStr
|
||||
esc = !esc && c == '\\' && inStr == '"'
|
||||
b[i] = ' '
|
||||
if wasEnd {
|
||||
inStr = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch c {
|
||||
case '"', '\'':
|
||||
inStr = c
|
||||
b[i] = ' '
|
||||
case '-':
|
||||
if i+1 < len(b) && b[i+1] == '-' {
|
||||
for ; i < len(b); i++ {
|
||||
b[i] = ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func luaStripLineComment(line string) string {
|
||||
inStr := byte(0)
|
||||
esc := false
|
||||
for i := 0; i+1 < len(line); i++ {
|
||||
c := line[i]
|
||||
if inStr != 0 {
|
||||
switch {
|
||||
case esc:
|
||||
esc = false
|
||||
case c == '\\' && inStr == '"':
|
||||
esc = true
|
||||
case c == inStr:
|
||||
inStr = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch c {
|
||||
case '"', '\'':
|
||||
inStr = c
|
||||
case '-':
|
||||
if line[i+1] == '-' {
|
||||
return line[:i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func luaUnwrapCall(op, fn string) (string, bool) {
|
||||
op = strings.TrimSpace(op)
|
||||
if !strings.HasPrefix(op, fn) {
|
||||
return "", false
|
||||
}
|
||||
rest := strings.TrimSpace(op[len(fn):])
|
||||
if !strings.HasPrefix(rest, "(") || !strings.HasSuffix(rest, ")") {
|
||||
return "", false
|
||||
}
|
||||
return rest[1 : len(rest)-1], true
|
||||
}
|
||||
|
||||
func isLuaPlainStringArg(expr string) bool {
|
||||
expr = strings.TrimSpace(expr)
|
||||
if expr == "" || (expr[0] != '"' && expr[0] != '\'') {
|
||||
return false
|
||||
}
|
||||
_, next, ok := parseLuaStringLiteral(expr, 0)
|
||||
return ok && next == len(expr)
|
||||
}
|
||||
|
||||
func isLuaIdentStart(c byte) bool {
|
||||
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
func cloneLuaEnv(env luaVarEnv) luaVarEnv {
|
||||
clone := make(luaVarEnv, len(env))
|
||||
maps.Copy(clone, env)
|
||||
return clone
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpandLuaConfigLinesVariableConcat(t *testing.T) {
|
||||
lines := []string{
|
||||
`local mainMod = "SUPER"`,
|
||||
`hl.bind(mainMod .. " + C", hl.dsp.window.close())`,
|
||||
`hl.bind(mainMod .. " + H", hl.dsp.focus({direction = "l"}))`,
|
||||
`hl.bind("ALT + TAB", hl.dsp.window.cycle_next({}))`,
|
||||
}
|
||||
got := expandLuaConfigLines(lines)
|
||||
|
||||
want := []string{
|
||||
`hl.bind("SUPER + C",`,
|
||||
`hl.bind("SUPER + H",`,
|
||||
`hl.bind("ALT + TAB",`,
|
||||
}
|
||||
joined := strings.Join(got, "\n")
|
||||
for _, w := range want {
|
||||
if !strings.Contains(joined, w) {
|
||||
t.Errorf("expanded output missing %q\n---\n%s", w, joined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandLuaConfigLinesForLoop(t *testing.T) {
|
||||
lines := []string{
|
||||
`local mainMod = "SUPER"`,
|
||||
`for i = 1, 3 do`,
|
||||
` hl.bind(mainMod .. " + " .. i, hl.dsp.focus({workspace = tostring(i)}))`,
|
||||
` hl.bind(mainMod .. " SHIFT + " .. i, hl.dsp.window.move({workspace = tostring(i)}))`,
|
||||
`end`,
|
||||
}
|
||||
got := strings.Join(expandLuaConfigLines(lines), "\n")
|
||||
|
||||
for _, w := range []string{
|
||||
`hl.bind("SUPER + 1",`,
|
||||
`hl.bind("SUPER + 2",`,
|
||||
`hl.bind("SUPER + 3",`,
|
||||
`hl.bind("SUPER SHIFT + 3",`,
|
||||
`{workspace = "1"}`,
|
||||
} {
|
||||
if !strings.Contains(got, w) {
|
||||
t.Errorf("expanded loop missing %q\n---\n%s", w, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLuaLinesDynamicBinds(t *testing.T) {
|
||||
content := strings.Join([]string{
|
||||
`local mainMod = "SUPER"`,
|
||||
`hl.bind(mainMod .. " + C", hl.dsp.window.close())`,
|
||||
`hl.bind("ALT + TAB", hl.dsp.window.cycle_next({}))`,
|
||||
`for i = 1, 2 do`,
|
||||
` hl.bind(mainMod .. " + " .. i, hl.dsp.focus({workspace = tostring(i)}))`,
|
||||
`end`,
|
||||
}, "\n")
|
||||
|
||||
parser := NewHyprlandParser("")
|
||||
section, err := parser.parseLuaLines(content, "", "test.lua", "")
|
||||
if err != nil {
|
||||
t.Fatalf("parseLuaLines: %v", err)
|
||||
}
|
||||
|
||||
keys := map[string]*HyprlandKeyBinding{}
|
||||
for i := range section.Keybinds {
|
||||
kb := §ion.Keybinds[i]
|
||||
keys[parser.formatBindKey(kb)] = kb
|
||||
}
|
||||
|
||||
for _, want := range []string{"SUPER+C", "ALT+TAB", "SUPER+1", "SUPER+2"} {
|
||||
if _, ok := keys[want]; !ok {
|
||||
t.Errorf("missing bind %q; got %v", want, keysList(keys))
|
||||
}
|
||||
}
|
||||
if kb := keys["SUPER+C"]; kb != nil && kb.Dispatcher != "killactive" {
|
||||
t.Errorf("SUPER+C dispatcher = %q, want killactive", kb.Dispatcher)
|
||||
}
|
||||
if kb := keys["SUPER+1"]; kb != nil {
|
||||
if kb.Dispatcher != "workspace" || kb.Params != "1" {
|
||||
t.Errorf("SUPER+1 = %q %q, want workspace 1", kb.Dispatcher, kb.Params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func keysList(m map[string]*HyprlandKeyBinding) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestEvalLuaConcat(t *testing.T) {
|
||||
env := luaVarEnv{"mainMod": "SUPER", "i": "5"}
|
||||
tests := []struct {
|
||||
expr string
|
||||
want string
|
||||
ok bool
|
||||
}{
|
||||
{`mainMod .. " + C"`, "SUPER + C", true},
|
||||
{`mainMod .. " + " .. i`, "SUPER + 5", true},
|
||||
{`mainMod .. " + " .. tostring(i)`, "SUPER + 5", true},
|
||||
{`"ALT + TAB"`, "ALT + TAB", true},
|
||||
{`mainMod .. someFunc()`, "", false},
|
||||
{`unknownVar .. "x"`, "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, ok := evalLuaConcat(tt.expr, env)
|
||||
if ok != tt.ok || (ok && got != tt.want) {
|
||||
t.Errorf("evalLuaConcat(%q) = %q,%v want %q,%v", tt.expr, got, ok, tt.want, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubstituteLuaIdent(t *testing.T) {
|
||||
tests := []struct {
|
||||
line, name, value, want string
|
||||
}{
|
||||
{`hl.bind(m .. " + " .. i, x)`, "i", "3", `hl.bind(m .. " + " .. 3, x)`},
|
||||
{`hl.dsp.exec("light -i")`, "i", "3", `hl.dsp.exec("light -i")`},
|
||||
{`foo.i`, "i", "3", `foo.i`},
|
||||
{`tostring(i)`, "i", "3", `tostring(3)`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := substituteLuaIdent(tt.line, tt.name, tt.value); got != tt.want {
|
||||
t.Errorf("substituteLuaIdent(%q,%q,%q) = %q, want %q", tt.line, tt.name, tt.value, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,7 +623,7 @@ func (p *HyprlandParser) parseLuaLines(content string, baseDir, absPath, section
|
||||
prevSource := p.currentSource
|
||||
p.currentSource = absPath
|
||||
|
||||
lines := expandLuaConfigLines(strings.Split(content, "\n"))
|
||||
lines := strings.Split(content, "\n")
|
||||
boundInFile := make(map[string]bool)
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
@@ -68,8 +68,6 @@ func GetQtLoggingRules() string {
|
||||
level = "info"
|
||||
}
|
||||
|
||||
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
|
||||
// probes); suppressed except at debug level
|
||||
var rules []string
|
||||
switch strings.ToLower(level) {
|
||||
case "fatal":
|
||||
@@ -77,13 +75,13 @@ func GetQtLoggingRules() string {
|
||||
case "error":
|
||||
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
|
||||
case "warn", "warning":
|
||||
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"}
|
||||
rules = []string{"*.debug=false", "*.info=false"}
|
||||
case "info":
|
||||
rules = []string{"*.debug=false", "scene.warning=false"}
|
||||
rules = []string{"*.debug=false"}
|
||||
case "debug":
|
||||
return ""
|
||||
default:
|
||||
rules = []string{"*.debug=false", "scene.warning=false"}
|
||||
rules = []string{"*.debug=false"}
|
||||
}
|
||||
|
||||
return strings.Join(rules, ";")
|
||||
|
||||
@@ -1056,9 +1056,6 @@ func closestAdwaitaAccent(primaryHex string) string {
|
||||
|
||||
func syncAccentColor(primaryHex string) {
|
||||
accent := closestAdwaitaAccent(primaryHex)
|
||||
if cur, err := utils.GsettingsGet("org.gnome.desktop.interface", "accent-color"); err == nil && strings.Trim(cur, "'") == accent {
|
||||
return
|
||||
}
|
||||
log.Infof("Setting GNOME accent color: %s", accent)
|
||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil {
|
||||
log.Warnf("Failed to set accent-color: %v", err)
|
||||
|
||||
@@ -545,18 +545,12 @@ func (a *SecretAgent) GetSecrets(
|
||||
out[settingName] = sec
|
||||
}
|
||||
if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || reply.Save) {
|
||||
secrets := make(map[string]string)
|
||||
for k, v := range reply.Secrets {
|
||||
if k != "username" {
|
||||
secrets[k] = v
|
||||
}
|
||||
}
|
||||
pw := reply.Secrets["password"]
|
||||
a.backend.pendingVPNSaveMu.Lock()
|
||||
a.backend.pendingVPNSave = &pendingVPNCredentials{
|
||||
ConnectionPath: string(path),
|
||||
Username: vpnUsername,
|
||||
Password: reply.Secrets["password"],
|
||||
Secrets: secrets,
|
||||
Password: pw,
|
||||
SavePassword: reply.Save,
|
||||
}
|
||||
a.backend.pendingVPNSaveMu.Unlock()
|
||||
@@ -796,23 +790,7 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
||||
fields = []string{"username", "password"}
|
||||
}
|
||||
case strings.Contains(vpnService, "openvpn"):
|
||||
switch connType {
|
||||
case "tls":
|
||||
// Cert auth wants the private-key passphrase, not a password.
|
||||
if secretFlagsNotRequired(dataMap["cert-pass-flags"]) {
|
||||
return nil
|
||||
}
|
||||
return []string{"cert-pass"}
|
||||
case "password-tls":
|
||||
fields = []string{}
|
||||
if dataMap["username"] == "" {
|
||||
fields = append(fields, "username")
|
||||
}
|
||||
fields = append(fields, "password")
|
||||
if !secretFlagsNotRequired(dataMap["cert-pass-flags"]) {
|
||||
fields = append(fields, "cert-pass")
|
||||
}
|
||||
case "password":
|
||||
if connType == "password" || connType == "password-tls" {
|
||||
if dataMap["username"] == "" {
|
||||
fields = []string{"username", "password"}
|
||||
}
|
||||
@@ -827,19 +805,6 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
||||
return fields
|
||||
}
|
||||
|
||||
// NetworkManager secret flags: bit 1 (value 2) marks a secret as not-saved,
|
||||
// bit 2 (value 4) as not-required. A not-required secret must not be prompted.
|
||||
func secretFlagsNotRequired(flags string) bool {
|
||||
if flags == "" {
|
||||
return false
|
||||
}
|
||||
n, err := strconv.Atoi(flags)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return n&0x4 != 0
|
||||
}
|
||||
|
||||
func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool {
|
||||
if method, ok := data["saml-auth-method"]; ok {
|
||||
if method == "REDIRECT" || method == "POST" {
|
||||
|
||||
@@ -315,44 +315,6 @@ func TestInferVPNFields_GPSaml(t *testing.T) {
|
||||
expectedLen: 1,
|
||||
shouldHave: []string{"password"},
|
||||
},
|
||||
{
|
||||
name: "OpenVPN cert auth (tls) - private-key passphrase",
|
||||
vpnService: "org.freedesktop.NetworkManager.openvpn",
|
||||
dataMap: map[string]string{
|
||||
"connection-type": "tls",
|
||||
},
|
||||
expectedLen: 1,
|
||||
shouldHave: []string{"cert-pass"},
|
||||
},
|
||||
{
|
||||
name: "OpenVPN cert auth (tls) with passphrase-less key",
|
||||
vpnService: "org.freedesktop.NetworkManager.openvpn",
|
||||
dataMap: map[string]string{
|
||||
"connection-type": "tls",
|
||||
"cert-pass-flags": "4",
|
||||
},
|
||||
expectedLen: 0,
|
||||
},
|
||||
{
|
||||
name: "OpenVPN password-tls no username",
|
||||
vpnService: "org.freedesktop.NetworkManager.openvpn",
|
||||
dataMap: map[string]string{
|
||||
"connection-type": "password-tls",
|
||||
},
|
||||
expectedLen: 3,
|
||||
shouldHave: []string{"username", "password", "cert-pass"},
|
||||
},
|
||||
{
|
||||
name: "OpenVPN password-tls with username, passphrase-less key",
|
||||
vpnService: "org.freedesktop.NetworkManager.openvpn",
|
||||
dataMap: map[string]string{
|
||||
"connection-type": "password-tls",
|
||||
"username": "john",
|
||||
"cert-pass-flags": "4",
|
||||
},
|
||||
expectedLen: 1,
|
||||
shouldHave: []string{"password"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -90,10 +90,7 @@ type pendingVPNCredentials struct {
|
||||
ConnectionPath string
|
||||
Username string
|
||||
Password string
|
||||
// Secrets holds all VPN secret fields keyed by name (e.g. "cert-pass");
|
||||
// falls back to Password under the "password" key when empty.
|
||||
Secrets map[string]string
|
||||
SavePassword bool
|
||||
SavePassword bool
|
||||
}
|
||||
|
||||
type cachedVPNCredentials struct {
|
||||
|
||||
@@ -863,17 +863,13 @@ func (b *NetworkManagerBackend) saveVPNCredentials(creds *pendingVPNCredentials)
|
||||
log.Infof("[saveVPNCredentials] Saving username")
|
||||
}
|
||||
|
||||
// Save secrets if requested
|
||||
// Save password if requested
|
||||
if creds.SavePassword {
|
||||
secs := creds.Secrets
|
||||
if len(secs) == 0 {
|
||||
secs = map[string]string{"password": creds.Password}
|
||||
}
|
||||
for field := range secs {
|
||||
data[field+"-flags"] = "0"
|
||||
}
|
||||
data["password-flags"] = "0"
|
||||
secs := make(map[string]string)
|
||||
secs["password"] = creds.Password
|
||||
vpn["secrets"] = dbus.MakeVariant(secs)
|
||||
log.Infof("[saveVPNCredentials] Saving %d secret field(s) with flags=0", len(secs))
|
||||
log.Infof("[saveVPNCredentials] Saving password with password-flags=0")
|
||||
}
|
||||
|
||||
vpn["data"] = dbus.MakeVariant(data)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
@@ -130,7 +129,7 @@ func (m Model) deployConfigurations() tea.Cmd {
|
||||
|
||||
deployer := config.NewConfigDeployer(m.logChan)
|
||||
|
||||
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems, m.useSystemdConfig())
|
||||
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems)
|
||||
|
||||
return configDeploymentResult{
|
||||
results: results,
|
||||
@@ -139,29 +138,6 @@ func (m Model) deployConfigurations() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) optionalDepSelected(name string) bool {
|
||||
if m.disabledItems[name] {
|
||||
return false
|
||||
}
|
||||
for _, dep := range m.dependencies {
|
||||
if dep.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Model) useSystemdConfig() bool {
|
||||
if m.osInfo == nil {
|
||||
return true
|
||||
}
|
||||
distroConfig, exists := distros.Registry[m.osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
return distroConfig.Family != distros.FamilyVoid
|
||||
}
|
||||
|
||||
func (m Model) viewConfigConfirmation() string {
|
||||
var b strings.Builder
|
||||
|
||||
@@ -219,50 +195,12 @@ func (m Model) viewConfigConfirmation() string {
|
||||
b.WriteString(backup)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if note := m.configReplacementNote(); note != "" {
|
||||
b.WriteString(m.styles.Subtle.Render(note))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) configReplacementNote() string {
|
||||
if m.selectedConfig < 0 || m.selectedConfig >= len(m.existingConfigs) {
|
||||
return ""
|
||||
}
|
||||
configInfo := m.existingConfigs[m.selectedConfig]
|
||||
if !configInfo.Exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch configInfo.ConfigType {
|
||||
case "Niri":
|
||||
if m.useSystemdConfig() {
|
||||
return "Replacing Niri writes the DMS Niri template and uses the user systemd dms service for shell autostart."
|
||||
}
|
||||
return `Replacing Niri writes the DMS Niri template and starts DMS from Niri with spawn-at-startup "dms" "run".`
|
||||
case "Hyprland":
|
||||
if m.useSystemdConfig() {
|
||||
return "Replacing Hyprland writes the DMS Lua template and uses the user systemd dms service for shell autostart."
|
||||
}
|
||||
return `Replacing Hyprland writes the DMS Lua template and starts DMS from Hyprland with hl.exec_cmd("dms run").`
|
||||
case "Mango":
|
||||
return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with exec-once=dms run."
|
||||
case "Ghostty":
|
||||
return "Replacing Ghostty writes the DMS terminal defaults and theme include."
|
||||
case "Kitty":
|
||||
return "Replacing Kitty writes the DMS terminal defaults, theme include, and tab styling."
|
||||
case "Alacritty":
|
||||
return "Replacing Alacritty writes the DMS terminal defaults and theme import."
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if result, ok := msg.(configCheckResult); ok {
|
||||
if result.error != nil {
|
||||
|
||||
@@ -28,21 +28,6 @@ func (m Model) viewDetectingDeps() string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func partitionOptionalLast(dependencies []deps.Dependency) []deps.Dependency {
|
||||
ordered := make([]deps.Dependency, 0, len(dependencies))
|
||||
for _, dep := range dependencies {
|
||||
if dep.Required {
|
||||
ordered = append(ordered, dep)
|
||||
}
|
||||
}
|
||||
for _, dep := range dependencies {
|
||||
if !dep.Required {
|
||||
ordered = append(ordered, dep)
|
||||
}
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
func (m Model) viewDependencyReview() string {
|
||||
var b strings.Builder
|
||||
|
||||
@@ -54,15 +39,7 @@ func (m Model) viewDependencyReview() string {
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(m.dependencies) > 0 {
|
||||
optionalHeaderShown := false
|
||||
for i, dep := range m.dependencies {
|
||||
if !dep.Required && !optionalHeaderShown {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.styles.Subtle.Render("Optional (space to enable)"))
|
||||
b.WriteString("\n")
|
||||
optionalHeaderShown = true
|
||||
}
|
||||
|
||||
var status string
|
||||
var reinstallMarker string
|
||||
var variantMarker string
|
||||
@@ -105,13 +82,8 @@ func (m Model) viewDependencyReview() string {
|
||||
}
|
||||
|
||||
note := ""
|
||||
switch dep.Name {
|
||||
case "dms-greeter":
|
||||
if dep.Name == "dms-greeter" {
|
||||
note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
|
||||
case "danksearch":
|
||||
note = m.styles.Subtle.Render(" (file search; enables dsearch.service)")
|
||||
case "dankcalendar":
|
||||
note = m.styles.Subtle.Render(" (autostart managed in dankcalendar settings)")
|
||||
}
|
||||
|
||||
var line string
|
||||
@@ -148,13 +120,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.err = depsMsg.err
|
||||
m.state = StateError
|
||||
} else {
|
||||
m.dependencies = partitionOptionalLast(depsMsg.deps)
|
||||
// Optional components are opt-in, skipped by default
|
||||
for _, dep := range m.dependencies {
|
||||
if dep.Required {
|
||||
continue
|
||||
m.dependencies = depsMsg.deps
|
||||
// dms-greeter is opt-in skipped by default
|
||||
for _, dep := range depsMsg.deps {
|
||||
if dep.Name == "dms-greeter" {
|
||||
m.disabledItems["dms-greeter"] = true
|
||||
break
|
||||
}
|
||||
m.disabledItems[dep.Name] = true
|
||||
}
|
||||
m.state = StateDependencyReview
|
||||
}
|
||||
@@ -259,7 +231,14 @@ func (m Model) installPackages() tea.Cmd {
|
||||
for msg := range installerProgressChan {
|
||||
// Run optional greeter setup
|
||||
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
|
||||
if m.optionalDepSelected("dms-greeter") {
|
||||
greeterSelected := false
|
||||
for _, dep := range m.dependencies {
|
||||
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
|
||||
greeterSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if greeterSelected {
|
||||
compositorName := "niri"
|
||||
switch m.selectedWindowManager() {
|
||||
case deps.WindowManagerHyprland:
|
||||
@@ -286,28 +265,6 @@ func (m Model) installPackages() tea.Cmd {
|
||||
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
if m.useSystemdConfig() && m.optionalDepSelected("danksearch") {
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.97,
|
||||
step: "Enabling danksearch service...",
|
||||
logOutput: "Setting up dsearch.service...",
|
||||
}
|
||||
dsearchLogFunc := func(line string) {
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.97,
|
||||
step: "Enabling danksearch service...",
|
||||
logOutput: line,
|
||||
}
|
||||
}
|
||||
if err := distros.SetupDsearchService(context.Background(), dsearchLogFunc); err != nil {
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.98,
|
||||
step: "danksearch service warning",
|
||||
logOutput: fmt.Sprintf("danksearch service setup warning (non-fatal): %v", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tuiMsg := packageInstallProgressMsg{
|
||||
|
||||
@@ -140,7 +140,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
||||
return "dms-shell-git"
|
||||
}
|
||||
return "dms-shell"
|
||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE, distros.FamilyVoid:
|
||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||
if isGit {
|
||||
return "dms-git"
|
||||
}
|
||||
@@ -168,8 +168,6 @@ func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
|
||||
return "sudo apt remove " + pkg
|
||||
case distros.FamilySUSE:
|
||||
return "sudo zypper remove " + pkg
|
||||
case distros.FamilyVoid:
|
||||
return "sudo xbps-remove -R " + pkg
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -203,26 +201,15 @@ func (m Model) viewInstallComplete() string {
|
||||
|
||||
wm := m.selectedWindowManager()
|
||||
|
||||
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
|
||||
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
|
||||
if !m.useSystemdConfig() {
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session niri"
|
||||
case deps.WindowManagerHyprland:
|
||||
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session Hyprland"
|
||||
case deps.WindowManagerMango:
|
||||
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session mango"
|
||||
}
|
||||
} else {
|
||||
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
loginHint = "If you do not have a greeter, login with \"niri-session\""
|
||||
case deps.WindowManagerHyprland:
|
||||
loginHint = "If you do not have a greeter, login with \"Hyprland\""
|
||||
case deps.WindowManagerMango:
|
||||
loginHint = "If you do not have a greeter, login with \"mango\""
|
||||
}
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
loginHint = "If you do not have a greeter, login with \"niri-session\""
|
||||
case deps.WindowManagerHyprland:
|
||||
loginHint = "If you do not have a greeter, login with \"Hyprland\""
|
||||
case deps.WindowManagerMango:
|
||||
loginHint = "If you do not have a greeter, login with \"mango\""
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
@@ -235,17 +222,7 @@ func (m Model) viewInstallComplete() string {
|
||||
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
|
||||
|
||||
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
|
||||
if !m.useSystemdConfig() {
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove spawn-at-startup "dms" "run" from ~/.config/niri/config.kdl`) + "\n")
|
||||
case deps.WindowManagerHyprland:
|
||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`) + "\n")
|
||||
case deps.WindowManagerMango:
|
||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
|
||||
}
|
||||
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("quickshell --path ~/.config/quickshell/dms log") + "\n")
|
||||
} else if wm == deps.WindowManagerMango {
|
||||
if wm == deps.WindowManagerMango {
|
||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
|
||||
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ in {
|
||||
text = lib.pipe cfg'.filesToInclude [
|
||||
(map (filename: "dms/${filename}"))
|
||||
withOriginalConfig
|
||||
(map (filename: "include optional=true \"${filename}.kdl\""))
|
||||
(map (filename: "include \"${filename}.kdl\""))
|
||||
(files: files ++ fixes)
|
||||
(builtins.concatStringsSep "\n")
|
||||
];
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
let
|
||||
homeManagerNixosModule =
|
||||
(fetchTarball {
|
||||
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
|
||||
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
|
||||
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
|
||||
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
|
||||
})
|
||||
+ "/nixos";
|
||||
in
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
let
|
||||
homeManagerNixosModule =
|
||||
(fetchTarball {
|
||||
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
|
||||
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
|
||||
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
|
||||
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
|
||||
})
|
||||
+ "/nixos";
|
||||
|
||||
@@ -76,8 +76,8 @@ pkgs.testers.runNixOSTest {
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
machine.succeed("su -- danklinux -c 'test -f ~/.config/niri/config.kdl'")
|
||||
machine.succeed("su -- danklinux -c 'grep -F \"include optional=true \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'")
|
||||
machine.succeed("su -- danklinux -c 'grep -F \"include optional=true \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'")
|
||||
machine.succeed("su -- danklinux -c 'grep -F \"include \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'")
|
||||
machine.succeed("su -- danklinux -c 'grep -F \"include \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'")
|
||||
machine.succeed("su -- danklinux -c 'grep -F \"spawn-at-startup\" ~/.config/niri/hm.kdl'")
|
||||
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
|
||||
'';
|
||||
|
||||
+2
-23
@@ -52,8 +52,8 @@ checkout at `srcpkgs/<pkg>/template` to build or submit it.
|
||||
## Dependencies
|
||||
|
||||
Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`,
|
||||
`matugen` (which drives the Material You theming), `dbus`, and `elogind`.
|
||||
The rest are optional, install whichever features you want:
|
||||
and `matugen` (which drives the Material You theming). The rest are optional —
|
||||
install whichever features you want:
|
||||
|
||||
| Package | Enables |
|
||||
| --- | --- |
|
||||
@@ -98,27 +98,6 @@ spawn-at-startup "dms" "run"
|
||||
|
||||
or Hyprland: `exec-once = dms run`.
|
||||
|
||||
From a TTY on Void without a greeter, start your compositor through a D-Bus
|
||||
session:
|
||||
|
||||
```sh
|
||||
dbus-run-session niri
|
||||
dbus-run-session Hyprland
|
||||
dbus-run-session mango
|
||||
```
|
||||
|
||||
The `mangowc` package provides the `mango` command.
|
||||
|
||||
For power menu actions to work on runit systems, make sure the system D-Bus and
|
||||
elogind services are enabled:
|
||||
|
||||
```sh
|
||||
sudo ln -sf /etc/sv/dbus /var/service/dbus
|
||||
sudo ln -sf /etc/sv/elogind /var/service/elogind
|
||||
```
|
||||
|
||||
The `dankinstall` Void path does this automatically after installing packages.
|
||||
|
||||
## Greeter (optional)
|
||||
|
||||
Install `dms-greeter`, then let the CLI do the setup:
|
||||
|
||||
@@ -32,12 +32,13 @@ conflicts="dms"
|
||||
provides="dms-${version}_${revision}"
|
||||
|
||||
# Optional feature deps are listed in distro/void/README.md.
|
||||
depends="quickshell accountsservice dgop matugen dbus elogind"
|
||||
depends="quickshell accountsservice dgop matugen dbus"
|
||||
|
||||
post_install() {
|
||||
# QML shell tree (build_style=go already installed the dms binary)
|
||||
vmkdir usr/share/quickshell/dms
|
||||
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
||||
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
|
||||
|
||||
# Desktop entry + icon
|
||||
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
||||
|
||||
@@ -22,12 +22,13 @@ distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v$
|
||||
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
|
||||
|
||||
# Optional feature deps are listed in distro/void/README.md.
|
||||
depends="quickshell accountsservice dgop matugen dbus elogind"
|
||||
depends="quickshell accountsservice dgop matugen dbus"
|
||||
|
||||
post_install() {
|
||||
# QML shell tree (build_style=go already installed the dms binary)
|
||||
vmkdir usr/share/quickshell/dms
|
||||
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
||||
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
|
||||
|
||||
# Desktop entry + icon
|
||||
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
||||
|
||||
Generated
+3
-3
@@ -18,11 +18,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1783224372,
|
||||
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
|
||||
"lastModified": 1778443072,
|
||||
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
|
||||
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -138,8 +138,11 @@
|
||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||
|
||||
# Snapshot pre-wrap Qt paths for launched apps to restore.
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--run 'export DMS_ORIG_NIXPKGS_QT6_QML_IMPORT_PATH="''${NIXPKGS_QT6_QML_IMPORT_PATH:-}"' \
|
||||
--run 'export DMS_ORIG_QT_PLUGIN_PATH="''${QT_PLUGIN_PATH:-}"' \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
@@ -20,8 +19,6 @@ Singleton {
|
||||
|
||||
readonly property url imagecache: `${cache}/imagecache`
|
||||
|
||||
Component.onCompleted: mkdir(imagecache)
|
||||
|
||||
function stringify(path: url): string {
|
||||
return path.toString().replace(/%20/g, " ");
|
||||
}
|
||||
|
||||
@@ -120,18 +120,12 @@ Singleton {
|
||||
|
||||
function setMonitorScrollPosition(screenName, scrollX, scrollY) {
|
||||
var newPositions = Object.assign({}, monitorScrollPositions);
|
||||
newPositions[screenName] = {
|
||||
scrollX: scrollX,
|
||||
scrollY: scrollY
|
||||
};
|
||||
newPositions[screenName] = { scrollX: scrollX, scrollY: scrollY };
|
||||
monitorScrollPositions = newPositions;
|
||||
}
|
||||
|
||||
function getMonitorScrollPosition(screenName) {
|
||||
return monitorScrollPositions[screenName] || {
|
||||
scrollX: 50,
|
||||
scrollY: 50
|
||||
};
|
||||
return monitorScrollPositions[screenName] || { scrollX: 50, scrollY: 50 };
|
||||
}
|
||||
|
||||
function clearMonitorScrollPosition(screenName) {
|
||||
@@ -215,8 +209,6 @@ Singleton {
|
||||
property string locale: ""
|
||||
property string timeLocale: ""
|
||||
|
||||
property string notepadLastMode: ""
|
||||
|
||||
property string launcherLastMode: "all"
|
||||
property string launcherLastFileSearchType: "all"
|
||||
property string launcherLastQuery: ""
|
||||
@@ -1224,13 +1216,6 @@ Singleton {
|
||||
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
||||
}
|
||||
|
||||
function setNotepadLastMode(mode) {
|
||||
if (notepadLastMode === mode)
|
||||
return;
|
||||
notepadLastMode = mode;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setLauncherLastMode(mode) {
|
||||
launcherLastMode = mode;
|
||||
saveSettings();
|
||||
|
||||
@@ -462,7 +462,7 @@ Singleton {
|
||||
property bool clockCompactMode: false
|
||||
property int focusedWindowSize: 1
|
||||
property bool focusedWindowCompactMode: false
|
||||
property bool focusedWindowShowIcon: true
|
||||
property bool focusedWindowShowIcon: false
|
||||
property bool runningAppsCompactMode: true
|
||||
property int barMaxVisibleApps: 0
|
||||
property int barMaxVisibleRunningApps: 0
|
||||
@@ -1027,6 +1027,17 @@ Singleton {
|
||||
}
|
||||
]
|
||||
|
||||
// Standalone bar xray is unsafe when windows can render beneath its surface
|
||||
function _standaloneBarXrayAvailable(configs) {
|
||||
const list = configs || [];
|
||||
const activeBars = list.filter(c => c && c.enabled && (c.visible ?? true));
|
||||
const gapsOverride = (typeof CompositorService !== "undefined" && CompositorService.isHyprland) ? hyprlandLayoutGapsOverride : niriLayoutGapsOverride;
|
||||
const layoutGaps = gapsOverride >= 0 ? gapsOverride : Math.max(4, (list[0]?.spacing ?? 4));
|
||||
return activeBars.every(c => !c.autoHide && !(c.useOverlayLayer ?? false) && (c.spacing ?? 4) + (c.bottomGap ?? 0) + layoutGaps >= 0);
|
||||
}
|
||||
|
||||
readonly property bool standaloneBarXrayAvailable: _standaloneBarXrayAvailable(barConfigs)
|
||||
|
||||
property bool desktopClockEnabled: false
|
||||
property string desktopClockStyle: "analog"
|
||||
property real desktopClockTransparency: 0.8
|
||||
@@ -2468,13 +2479,17 @@ Singleton {
|
||||
if (index === -1)
|
||||
return;
|
||||
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
|
||||
const barXrayTargetWasAvailable = _standaloneBarXrayAvailable(configs);
|
||||
if (updates.autoHide === false || updates.visible === false)
|
||||
setBarIpcReveal(barId, false);
|
||||
|
||||
Object.assign(configs[index], updates);
|
||||
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
|
||||
const sanitizedConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
|
||||
barConfigs = sanitizedConfigs;
|
||||
updateBarConfigs();
|
||||
|
||||
if (!frameEnabled && _standaloneBarXrayAvailable(sanitizedConfigs) !== barXrayTargetWasAvailable)
|
||||
updateCompositorLayout();
|
||||
if (positionChanged) {
|
||||
NotificationService.dismissAllPopups();
|
||||
}
|
||||
|
||||
@@ -88,8 +88,6 @@ var SPEC = {
|
||||
locale: { def: "", onChange: "updateLocale" },
|
||||
timeLocale: { def: "" },
|
||||
|
||||
notepadLastMode: { def: "" },
|
||||
|
||||
launcherLastMode: { def: "all" },
|
||||
launcherLastFileSearchType: { def: "all" },
|
||||
launcherLastQuery: { def: "" },
|
||||
|
||||
@@ -192,7 +192,7 @@ var SPEC = {
|
||||
clockCompactMode: { def: false },
|
||||
focusedWindowCompactMode: { def: false },
|
||||
focusedWindowSize: { def: 1 },
|
||||
focusedWindowShowIcon: { def: true },
|
||||
focusedWindowShowIcon: { def: false },
|
||||
runningAppsCompactMode: { def: true },
|
||||
barMaxVisibleApps: { def: 0 },
|
||||
barMaxVisibleRunningApps: { def: 0 },
|
||||
|
||||
@@ -373,7 +373,7 @@ Item {
|
||||
}
|
||||
|
||||
function open(): string {
|
||||
if (PopoutService.notepadResolvedMode === "popout") {
|
||||
if (SettingsData.notepadDefaultMode === "popout") {
|
||||
PopoutService.openNotepadPopout();
|
||||
return "NOTEPAD_OPEN_SUCCESS";
|
||||
}
|
||||
@@ -388,7 +388,7 @@ Item {
|
||||
function openFile(path: string): string {
|
||||
if (!path)
|
||||
return open();
|
||||
if (PopoutService.notepadResolvedMode === "popout") {
|
||||
if (SettingsData.notepadDefaultMode === "popout") {
|
||||
PopoutService.openNotepadPopoutWithFile(path);
|
||||
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
||||
}
|
||||
@@ -402,7 +402,7 @@ Item {
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (PopoutService.notepadResolvedMode === "popout") {
|
||||
if (SettingsData.notepadDefaultMode === "popout") {
|
||||
PopoutService.notepadPopout?.hide();
|
||||
return "NOTEPAD_CLOSE_SUCCESS";
|
||||
}
|
||||
@@ -415,7 +415,7 @@ Item {
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
if (PopoutService.notepadResolvedMode === "popout") {
|
||||
if (SettingsData.notepadDefaultMode === "popout") {
|
||||
PopoutService.toggleNotepadPopout();
|
||||
return "NOTEPAD_TOGGLE_SUCCESS";
|
||||
}
|
||||
@@ -712,13 +712,12 @@ Item {
|
||||
target: "hypr"
|
||||
}
|
||||
|
||||
// ! TODO - remove for v1.6
|
||||
IpcHandler {
|
||||
function wallpaper(): string {
|
||||
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
||||
if (bar) {
|
||||
bar.triggerWallpaperBrowser();
|
||||
return "WARN; deprecated, use dms ipc call dash toggle wallpaper instead";
|
||||
return "SUCCESS: Toggled wallpaper browser";
|
||||
}
|
||||
return "ERROR: Failed to toggle wallpaper browser";
|
||||
}
|
||||
@@ -1793,8 +1792,8 @@ Item {
|
||||
}
|
||||
|
||||
function open(): string {
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||
return "WINDOW_RULES_UNSUPPORTED_COMPOSITOR";
|
||||
if (!CompositorService.isNiri)
|
||||
return "WINDOW_RULES_NIRI_ONLY";
|
||||
root.windowRuleModalLoader.active = true;
|
||||
if (root.windowRuleModalLoader.item) {
|
||||
root.windowRuleModalLoader.item.show(getFocusedWindow());
|
||||
@@ -1812,8 +1811,8 @@ Item {
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||
return "WINDOW_RULES_UNSUPPORTED_COMPOSITOR";
|
||||
if (!CompositorService.isNiri)
|
||||
return "WINDOW_RULES_NIRI_ONLY";
|
||||
root.windowRuleModalLoader.active = true;
|
||||
if (root.windowRuleModalLoader.item) {
|
||||
if (root.windowRuleModalLoader.item.visible) {
|
||||
|
||||
@@ -65,7 +65,7 @@ Column {
|
||||
StyledText {
|
||||
id: codenameText
|
||||
anchors.centerIn: parent
|
||||
text: "The Wolverine"
|
||||
text: "Saffron Bloom"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primary
|
||||
@@ -74,7 +74,7 @@ Column {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Frame Mode, DankCalendar, Spotlight, & more"
|
||||
text: "New launcher, enhanced plugin system, KDE Connect, & more"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
@@ -108,99 +108,77 @@ Column {
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "border_outer"
|
||||
title: "Frame Mode"
|
||||
description: "Connected shell surfaces"
|
||||
onClicked: PopoutService.openSettingsWithTab("frame")
|
||||
iconName: "space_dashboard"
|
||||
title: "Dank Launcher V2"
|
||||
description: "New capabilities & plugins"
|
||||
onClicked: PopoutService.openDankLauncherV2()
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "calendar_month"
|
||||
title: "DankCalendar"
|
||||
description: "Native calendar & events"
|
||||
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dankcalendar")
|
||||
iconName: "smartphone"
|
||||
title: "Phone Connect"
|
||||
description: "KDE Connect & Valent"
|
||||
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "search"
|
||||
title: "Spotlight"
|
||||
description: "Lightweight launcher"
|
||||
onClicked: PopoutService.openSpotlightBar()
|
||||
iconName: "monitor_heart"
|
||||
title: "System Monitor"
|
||||
description: "Redesigned process list"
|
||||
onClicked: PopoutService.showProcessListModal()
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "window"
|
||||
title: "Window Rules"
|
||||
description: "Rules for many compositors"
|
||||
description: "niri window rule manager"
|
||||
visible: CompositorService.isNiri
|
||||
onClicked: PopoutService.openSettingsWithTab("window_rules")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "display_settings"
|
||||
title: "Display Profiles"
|
||||
description: "Auto-switch monitor layouts"
|
||||
onClicked: PopoutService.openSettingsWithTab("display_config")
|
||||
iconName: "notifications_active"
|
||||
title: "Enhanced Notifications"
|
||||
description: "Configurable rules & styling"
|
||||
visible: !CompositorService.isNiri
|
||||
onClicked: PopoutService.openSettingsWithTab("notifications")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "dvr"
|
||||
title: "Multiplexer Launcher"
|
||||
description: "Attach to tmux sessions"
|
||||
onClicked: PopoutService.openSettingsWithTab("multiplexers")
|
||||
iconName: "dock_to_bottom"
|
||||
title: "Dock Enhancements"
|
||||
description: "Bar dock widget & more"
|
||||
onClicked: PopoutService.openSettingsWithTab("dock")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "edit_note"
|
||||
title: "Notepad Rewrite"
|
||||
description: "Popout & tiling support"
|
||||
onClicked: PopoutService.openNotepad()
|
||||
iconName: "volume_up"
|
||||
title: "Audio Aliases"
|
||||
description: "Custom device names"
|
||||
onClicked: PopoutService.openSettingsWithTab("audio")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "gradient"
|
||||
title: "M3 Shadows"
|
||||
description: "Reworked elevation system"
|
||||
iconName: "extension"
|
||||
title: "Enhanced Plugin System"
|
||||
description: "Enables new types of plugins"
|
||||
onClicked: PopoutService.openSettingsWithTab("plugins")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "light_mode"
|
||||
title: "Auto Light/Dark"
|
||||
description: "Automatic mode switching"
|
||||
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "login"
|
||||
title: "Greeter Enhancements"
|
||||
description: "Settings GUI & multi-user"
|
||||
onClicked: PopoutService.openSettingsWithTab("greeter")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "content_paste"
|
||||
title: "Clipboard Filtering"
|
||||
description: "Text, image & pinned filters"
|
||||
onClicked: PopoutService.openSettingsWithTab("clipboard")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "restart_alt"
|
||||
title: "XDG Autostart"
|
||||
description: "Manage apps at login"
|
||||
onClicked: PopoutService.openSettingsWithTab("autostart")
|
||||
}
|
||||
|
||||
ChangelogFeatureCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "apps"
|
||||
title: "Default Apps"
|
||||
description: "Set preferred applications"
|
||||
onClicked: PopoutService.openSettingsWithTab("default_apps")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +230,12 @@ Column {
|
||||
|
||||
ChangelogUpgradeNote {
|
||||
width: parent.width
|
||||
text: "App ID changed to com.danklinux.dms — update any compositor window rules targeting the old ID"
|
||||
text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
|
||||
}
|
||||
|
||||
ChangelogUpgradeNote {
|
||||
width: parent.width
|
||||
text: "Plugin API updated — third-party plugins may need updates"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ FloatingWindow {
|
||||
iconName: "open_in_new"
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
textColor: Theme.surfaceText
|
||||
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-5-release")
|
||||
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
|
||||
}
|
||||
|
||||
DankButton {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
ClippingRectangle {
|
||||
Item {
|
||||
id: thumbnail
|
||||
readonly property var log: Log.scoped("ClipboardThumbnail")
|
||||
|
||||
@@ -15,10 +15,6 @@ ClippingRectangle {
|
||||
required property int itemIndex
|
||||
property bool disposed: false
|
||||
|
||||
radius: Theme.cornerRadius / 2
|
||||
color: "transparent"
|
||||
antialiasing: true
|
||||
|
||||
Image {
|
||||
id: thumbnailImage
|
||||
|
||||
@@ -38,7 +34,7 @@ ClippingRectangle {
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
cache: false
|
||||
visible: entryType === "image" && status === Image.Ready && source != ""
|
||||
visible: false
|
||||
asynchronous: true
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
@@ -240,6 +236,33 @@ ClippingRectangle {
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: thumbnailImage
|
||||
maskEnabled: true
|
||||
maskSource: clipboardRoundedRectangularMask
|
||||
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: clipboardRoundedRectangularMask
|
||||
width: ClipboardConstants.thumbnailSize
|
||||
height: ClipboardConstants.itemHeight - 4
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px)
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
||||
name: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -60,9 +61,12 @@ Item {
|
||||
}
|
||||
|
||||
// Hyprland OnDemand grab delivers keyboard focus to the modal content surface.
|
||||
DankFocusGrab {
|
||||
HyprlandFocusGrab {
|
||||
windows: (root.contentWindow ? [root.contentWindow] : []).concat(root.transientSurfaceTracker?.focusWindows ?? [])
|
||||
wanted: KeyboardFocus.wantsGrab(root.shouldHaveFocus, root.customKeyboardFocus)
|
||||
active: KeyboardFocus.wantsGrab(root.shouldHaveFocus, root.customKeyboardFocus)
|
||||
|
||||
property var restoreToplevel: null
|
||||
onActiveChanged: restoreToplevel = active ? KeyboardFocus.captureActiveToplevel() : KeyboardFocus.restoreToplevel(restoreToplevel)
|
||||
}
|
||||
readonly property var contentWindow: impl.item ? impl.item.contentWindow : null
|
||||
readonly property var effectiveScreen: impl.item ? impl.item.effectiveScreen : null
|
||||
|
||||
@@ -62,11 +62,6 @@ Item {
|
||||
|
||||
property bool animationsEnabled: true
|
||||
|
||||
function _kickBlurCommit() {
|
||||
if (typeof contentWindow.update === "function")
|
||||
contentWindow.update();
|
||||
}
|
||||
|
||||
function open() {
|
||||
closeTimer.stop();
|
||||
isClosing = false;
|
||||
@@ -206,12 +201,6 @@ Item {
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
onAlignedXChanged: _kickBlurCommit()
|
||||
onAlignedYChanged: _kickBlurCommit()
|
||||
onAlignedWidthChanged: _kickBlurCommit()
|
||||
onAlignedHeightChanged: _kickBlurCommit()
|
||||
onShouldBeVisibleChanged: _kickBlurCommit()
|
||||
|
||||
PanelWindow {
|
||||
id: clickCatcher
|
||||
visible: false
|
||||
@@ -255,13 +244,11 @@ Item {
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||
readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.06) * 2))
|
||||
readonly property real visibleScale: s * op
|
||||
// Blur tracks the surface's scaled rect, matching the connected backend.
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: root.shouldBeVisible ? modalContainer.width * visibleScale : 0
|
||||
blurHeight: root.shouldBeVisible ? modalContainer.height * visibleScale : 0
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: root.shouldBeVisible ? modalContainer.width * s : 0
|
||||
blurHeight: root.shouldBeVisible ? modalContainer.height * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
@@ -351,7 +338,6 @@ Item {
|
||||
QtObject {
|
||||
id: morph
|
||||
property real openProgress: root.shouldBeVisible ? 1 : 0
|
||||
onOpenProgressChanged: root._kickBlurCommit()
|
||||
Behavior on openProgress {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var editingApp: null
|
||||
property string editAppId: ""
|
||||
|
||||
signal closeRequested
|
||||
|
||||
function loadOverride() {
|
||||
var existing = SessionData.getAppOverride(editAppId);
|
||||
editNameField.text = existing?.name || "";
|
||||
editIconField.text = existing?.icon || "";
|
||||
editCommentField.text = existing?.comment || "";
|
||||
editEnvVarsField.text = existing?.envVars || "";
|
||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function saveAppOverride() {
|
||||
var override = {};
|
||||
if (editNameField.text.trim())
|
||||
override.name = editNameField.text.trim();
|
||||
if (editIconField.text.trim())
|
||||
override.icon = editIconField.text.trim();
|
||||
if (editCommentField.text.trim())
|
||||
override.comment = editCommentField.text.trim();
|
||||
if (editEnvVarsField.text.trim())
|
||||
override.envVars = editEnvVarsField.text.trim();
|
||||
if (editExtraFlagsField.text.trim())
|
||||
override.extraFlags = editExtraFlagsField.text.trim();
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeRequested();
|
||||
}
|
||||
|
||||
function resetAppOverride() {
|
||||
SessionData.clearAppOverride(editAppId);
|
||||
closeRequested();
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
closeRequested();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
|
||||
saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: backButtonArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.closeRequested()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
width: 40
|
||||
height: 40
|
||||
source: Paths.resolveIconUrl(root.editingApp?.icon || "application-x-executable")
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Edit App")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.editingApp?.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
}
|
||||
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - y - buttonsRow.height - Theme.spacingM
|
||||
contentHeight: editFieldsColumn.height
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: editFieldsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Name")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editNameField
|
||||
width: parent.width
|
||||
placeholderText: root.editingApp?.name || ""
|
||||
keyNavigationTab: editIconField
|
||||
keyNavigationBacktab: editExtraFlagsField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Icon")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editIconField
|
||||
width: parent.width
|
||||
placeholderText: root.editingApp?.icon || ""
|
||||
keyNavigationTab: editCommentField
|
||||
keyNavigationBacktab: editNameField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Description")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editCommentField
|
||||
width: parent.width
|
||||
placeholderText: root.editingApp?.comment || ""
|
||||
keyNavigationTab: editEnvVarsField
|
||||
keyNavigationBacktab: editIconField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Environment Variables")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "KEY=value KEY2=value2"
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editEnvVarsField
|
||||
width: parent.width
|
||||
placeholderText: "VAR=value"
|
||||
keyNavigationTab: editExtraFlagsField
|
||||
keyNavigationBacktab: editCommentField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Extra Arguments")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editExtraFlagsField
|
||||
width: parent.width
|
||||
placeholderText: "--flag --option=value"
|
||||
keyNavigationTab: editNameField
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonsRow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
id: resetButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
visible: SessionData.getAppOverride(root.editAppId) !== null
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reset")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.error
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resetButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.resetAppOverride()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cancelButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.closeRequested()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: saveButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: saveButtonArea.containsMouse ? Theme.withAlpha(Theme.primary, 0.9) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.saveAppOverride()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ Rectangle {
|
||||
property string cachedMimeType: ""
|
||||
property var _requestedEntryId: null
|
||||
|
||||
readonly property bool canLoadImage: typeof entry?.id === "number" && !!entry?.isImage && String(entry?.mimeType ?? "").startsWith("image/")
|
||||
readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
|
||||
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
|
||||
|
||||
radius: Math.max(6, Theme.cornerRadius - 2)
|
||||
@@ -41,18 +41,13 @@ Rectangle {
|
||||
}
|
||||
|
||||
function reloadPreview() {
|
||||
if (!canLoadImage || typeof entry?.id !== "number") {
|
||||
_requestedEntryId = null;
|
||||
cachedImageData = "";
|
||||
cachedMimeType = "";
|
||||
return;
|
||||
}
|
||||
// Entry objects are rebuilt per search; same id means same content
|
||||
if (entry.id === _requestedEntryId)
|
||||
return;
|
||||
|
||||
cachedImageData = "";
|
||||
cachedMimeType = "";
|
||||
if (!canLoadImage || !entry?.id) {
|
||||
_requestedEntryId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const entryId = entry.id;
|
||||
_requestedEntryId = entryId;
|
||||
DMSService.sendRequest("clipboard.getEntry", {
|
||||
@@ -60,22 +55,17 @@ Rectangle {
|
||||
}, function (response) {
|
||||
if (_requestedEntryId !== entryId)
|
||||
return;
|
||||
if (response.error) {
|
||||
_requestedEntryId = null;
|
||||
if (response.error)
|
||||
return;
|
||||
}
|
||||
if (!response.result) {
|
||||
_requestedEntryId = null;
|
||||
ClipboardService.refresh();
|
||||
return;
|
||||
}
|
||||
const result = response.result;
|
||||
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
|
||||
const data = (result.data ?? "").toString();
|
||||
if (data.length === 0 || !resolvedSourceUrl(data, mimeType)) {
|
||||
_requestedEntryId = null;
|
||||
if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
|
||||
return;
|
||||
}
|
||||
cachedMimeType = mimeType;
|
||||
cachedImageData = data;
|
||||
});
|
||||
@@ -88,8 +78,6 @@ Rectangle {
|
||||
asynchronous: true
|
||||
cache: false
|
||||
smooth: true
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
@@ -50,15 +50,15 @@ Item {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
ClipboardService.invalidateLauncherSearchCache();
|
||||
if (active)
|
||||
return;
|
||||
if (!active) {
|
||||
SessionData.addLauncherHistory(searchQuery);
|
||||
|
||||
SessionData.addLauncherHistory(searchQuery);
|
||||
sections = [];
|
||||
flatModel = [];
|
||||
selectedItem = null;
|
||||
_clearModeCache();
|
||||
sections = [];
|
||||
flatModel = [];
|
||||
selectedItem = null;
|
||||
_clearModeCache();
|
||||
ClipboardService.invalidateLauncherSearchCache();
|
||||
}
|
||||
}
|
||||
|
||||
onSearchModeChanged: {
|
||||
@@ -110,7 +110,7 @@ Item {
|
||||
if (query !== effectiveQuery)
|
||||
return;
|
||||
|
||||
root.requestSearch();
|
||||
searchDebounce.restart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,23 +276,9 @@ Item {
|
||||
property string appCategory: ""
|
||||
property var appCategories: []
|
||||
|
||||
function builtInSectionViewPref(sectionId) {
|
||||
switch (sectionId) {
|
||||
case "clipboard":
|
||||
return getPluginViewPref("dms_clipboard_search");
|
||||
case "settings":
|
||||
return getPluginViewPref("dms_settings_search");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getSectionViewMode(sectionId) {
|
||||
if (sectionId === "browse_plugins")
|
||||
return "list";
|
||||
var builtInPref = builtInSectionViewPref(sectionId);
|
||||
if (builtInPref?.enforced)
|
||||
return builtInPref.mode;
|
||||
if (pluginViewPreferences[sectionId]?.enforced)
|
||||
return pluginViewPreferences[sectionId].mode;
|
||||
if (sectionViewModes[sectionId])
|
||||
@@ -316,8 +302,6 @@ Item {
|
||||
function setSectionViewMode(sectionId, mode) {
|
||||
if (sectionId === "browse_plugins")
|
||||
return;
|
||||
if (builtInSectionViewPref(sectionId)?.enforced)
|
||||
return;
|
||||
if (pluginViewPreferences[sectionId]?.enforced)
|
||||
return;
|
||||
sectionViewModes = Object.assign({}, sectionViewModes, {
|
||||
@@ -341,8 +325,6 @@ Item {
|
||||
function canChangeSectionViewMode(sectionId) {
|
||||
if (sectionId === "browse_plugins")
|
||||
return false;
|
||||
if (builtInSectionViewPref(sectionId)?.enforced)
|
||||
return false;
|
||||
return !pluginViewPreferences[sectionId]?.enforced;
|
||||
}
|
||||
|
||||
@@ -398,29 +380,10 @@ Item {
|
||||
property bool _pluginPhaseForceFirst: false
|
||||
property var _phase1Items: []
|
||||
|
||||
property bool _searchPending: false
|
||||
|
||||
// Leading-edge debounce: search immediately when idle, coalesce bursts
|
||||
function requestSearch() {
|
||||
if (searchDebounce.running) {
|
||||
_searchPending = true;
|
||||
searchDebounce.restart();
|
||||
return;
|
||||
}
|
||||
_searchPending = false;
|
||||
performSearch();
|
||||
searchDebounce.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchDebounce
|
||||
interval: 60
|
||||
onTriggered: {
|
||||
if (!root._searchPending)
|
||||
return;
|
||||
root._searchPending = false;
|
||||
root.performSearch();
|
||||
}
|
||||
onTriggered: root.performSearch()
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -446,7 +409,7 @@ Item {
|
||||
_phase1Items = [];
|
||||
pluginPhaseTimer.stop();
|
||||
searchQuery = query;
|
||||
requestSearch();
|
||||
searchDebounce.restart();
|
||||
|
||||
if (searchMode !== "plugins" && query.startsWith("/")) {
|
||||
var prefix = Utils.parseFileSearchPrefix(query);
|
||||
@@ -731,7 +694,6 @@ Item {
|
||||
if (triggerMatch.isBuiltIn) {
|
||||
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
|
||||
for (var j = 0; j < builtInItems.length; j++) {
|
||||
builtInItems[j]._preScored = 1000 - j;
|
||||
allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId));
|
||||
}
|
||||
}
|
||||
@@ -930,10 +892,23 @@ Item {
|
||||
searchCompleted();
|
||||
return;
|
||||
} else if (!searchQuery) {
|
||||
_pluginPhasePending = true;
|
||||
_phase1Items = allItems.slice();
|
||||
_pluginPhaseForceFirst = shouldResetSelection;
|
||||
pluginPhaseTimer.restart();
|
||||
var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
|
||||
for (var i = 0; i < emptyTriggerOrdered.length; i++) {
|
||||
var plugin = emptyTriggerOrdered[i];
|
||||
if (plugin.isBuiltIn) {
|
||||
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
|
||||
for (var j = 0; j < blItems.length; j++)
|
||||
allItems.push(transformBuiltInSearchItem(blItems[j], plugin.id));
|
||||
} else {
|
||||
var pItems = getPluginItems(plugin.id, searchQuery);
|
||||
for (var j = 0; j < pItems.length; j++)
|
||||
allItems.push(pItems[j]);
|
||||
}
|
||||
}
|
||||
|
||||
var browseItems = getPluginBrowseItems();
|
||||
for (var i = 0; i < browseItems.length; i++)
|
||||
allItems.push(browseItems[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,6 +939,11 @@ Item {
|
||||
flatModel = Scorer.flattenSections(newSections);
|
||||
sections = newSections;
|
||||
|
||||
if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) {
|
||||
AppSearchService.setCachedDefaultSections(sections, flatModel);
|
||||
_saveDiskCache(sections);
|
||||
}
|
||||
|
||||
selectedFlatIndex = restoreSelection(flatModel);
|
||||
updateSelectedItem();
|
||||
|
||||
@@ -973,9 +953,7 @@ Item {
|
||||
|
||||
function _performPluginPhase() {
|
||||
_pluginPhasePending = false;
|
||||
if (searchMode !== "all")
|
||||
return;
|
||||
if (searchQuery && searchQuery.length < 2)
|
||||
if (!searchQuery || searchQuery.length < 2 || searchMode !== "all")
|
||||
return;
|
||||
|
||||
var currentVersion = _searchVersion;
|
||||
@@ -983,49 +961,27 @@ Item {
|
||||
var allItems = _phase1Items;
|
||||
_phase1Items = [];
|
||||
|
||||
if (!searchQuery) {
|
||||
var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
|
||||
for (var i = 0; i < emptyTriggerOrdered.length; i++) {
|
||||
if (currentVersion !== _searchVersion)
|
||||
return;
|
||||
var plugin = emptyTriggerOrdered[i];
|
||||
if (plugin.isBuiltIn) {
|
||||
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
|
||||
for (var j = 0; j < blItems.length; j++)
|
||||
allItems.push(transformBuiltInSearchItem(blItems[j], plugin.id));
|
||||
} else {
|
||||
var pItems = getPluginItems(plugin.id, searchQuery);
|
||||
for (var j = 0; j < pItems.length; j++)
|
||||
allItems.push(pItems[j]);
|
||||
var allPluginsOrdered = getAllVisiblePluginsOrdered();
|
||||
var maxPerPlugin = 10;
|
||||
for (var i = 0; i < allPluginsOrdered.length; i++) {
|
||||
if (currentVersion !== _searchVersion)
|
||||
return;
|
||||
var plugin = allPluginsOrdered[i];
|
||||
if (plugin.isBuiltIn && (plugin.id === "dms_settings_search" || plugin.id === "dms_clipboard_search"))
|
||||
continue;
|
||||
if (plugin.isBuiltIn) {
|
||||
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
|
||||
var blLimit = Math.min(blItems.length, maxPerPlugin);
|
||||
for (var j = 0; j < blLimit; j++) {
|
||||
var item = transformBuiltInSearchItem(blItems[j], plugin.id);
|
||||
item._preScored = 900 - j;
|
||||
allItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
var browseItems = getPluginBrowseItems();
|
||||
for (var i = 0; i < browseItems.length; i++)
|
||||
allItems.push(browseItems[i]);
|
||||
} else {
|
||||
var allPluginsOrdered = getAllVisiblePluginsOrdered();
|
||||
var maxPerPlugin = 10;
|
||||
for (var i = 0; i < allPluginsOrdered.length; i++) {
|
||||
if (currentVersion !== _searchVersion)
|
||||
return;
|
||||
var plugin = allPluginsOrdered[i];
|
||||
if (plugin.isBuiltIn && (plugin.id === "dms_settings_search" || plugin.id === "dms_clipboard_search"))
|
||||
continue;
|
||||
if (plugin.isBuiltIn) {
|
||||
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
|
||||
var blLimit = Math.min(blItems.length, maxPerPlugin);
|
||||
for (var j = 0; j < blLimit; j++) {
|
||||
var item = transformBuiltInSearchItem(blItems[j], plugin.id);
|
||||
item._preScored = 900 - j;
|
||||
allItems.push(item);
|
||||
}
|
||||
} else {
|
||||
var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin);
|
||||
for (var j = 0; j < pItems.length; j++) {
|
||||
pItems[j]._preScored = 900 - j;
|
||||
allItems.push(pItems[j]);
|
||||
}
|
||||
} else {
|
||||
var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin);
|
||||
for (var j = 0; j < pItems.length; j++) {
|
||||
pItems[j]._preScored = 900 - j;
|
||||
allItems.push(pItems[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1035,8 +991,7 @@ Item {
|
||||
|
||||
var dynamicDefs = buildDynamicSectionDefs(allItems);
|
||||
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
||||
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
|
||||
var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, searchQuery ? 50 : 500);
|
||||
var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, false, 50);
|
||||
|
||||
if (currentVersion !== _searchVersion)
|
||||
return;
|
||||
@@ -1050,12 +1005,6 @@ Item {
|
||||
_applyHighlights(newSections, searchQuery);
|
||||
flatModel = Scorer.flattenSections(newSections);
|
||||
sections = newSections;
|
||||
|
||||
if (!AppSearchService.isCacheValid() && !searchQuery && !pluginFilter) {
|
||||
AppSearchService.setCachedDefaultSections(sections, flatModel);
|
||||
_saveDiskCache(sections);
|
||||
}
|
||||
|
||||
selectedFlatIndex = restoreSelection(flatModel);
|
||||
updateSelectedItem();
|
||||
isSearching = false;
|
||||
@@ -1249,12 +1198,8 @@ Item {
|
||||
}
|
||||
|
||||
function transformBuiltInSearchItem(item, pluginId) {
|
||||
if (pluginId === "dms_clipboard_search" || item.type === "clipboard") {
|
||||
var transformed = transformClipboardEntry(item.data || item);
|
||||
if (item._preScored !== undefined)
|
||||
transformed._preScored = item._preScored;
|
||||
return transformed;
|
||||
}
|
||||
if (pluginId === "dms_clipboard_search" || item.type === "clipboard")
|
||||
return transformClipboardEntry(item.data || item);
|
||||
return transformBuiltInLauncherItem(item, pluginId);
|
||||
}
|
||||
|
||||
@@ -1926,8 +1871,7 @@ Item {
|
||||
}
|
||||
|
||||
function executeSelected() {
|
||||
if (_searchPending) {
|
||||
_searchPending = false;
|
||||
if (searchDebounce.running) {
|
||||
searchDebounce.stop();
|
||||
performSearch();
|
||||
}
|
||||
|
||||
@@ -120,18 +120,6 @@ Item {
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
||||
|
||||
// Blur region isn't auto-committed on geometry changes; kick twice to catch resize settling.
|
||||
function _kickBlurCommit() {
|
||||
launcherBlur.kick();
|
||||
Qt.callLater(launcherBlur.kick);
|
||||
}
|
||||
|
||||
onAlignedXChanged: _kickBlurCommit()
|
||||
onAlignedYChanged: _kickBlurCommit()
|
||||
onAlignedWidthChanged: _kickBlurCommit()
|
||||
on_ContentImplicitHChanged: _kickBlurCommit()
|
||||
onContentVisibleChanged: _kickBlurCommit()
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||
@@ -340,7 +328,6 @@ Item {
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WindowBlur {
|
||||
id: launcherBlur
|
||||
targetWindow: launcherWindow
|
||||
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
||||
blurX: modalContainer.x
|
||||
@@ -350,9 +337,6 @@ Item {
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
onWidthChanged: root._kickBlurCommit()
|
||||
onHeightChanged: root._kickBlurCommit()
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
@@ -437,9 +421,6 @@ Item {
|
||||
|
||||
opacity: contentVisible ? 1 : 0
|
||||
|
||||
onOpacityChanged: root._kickBlurCommit()
|
||||
onSlideOffsetChanged: root._kickBlurCommit()
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: contentVisible ? root._openDuration : root._closeDuration
|
||||
|
||||
@@ -82,12 +82,6 @@ Item {
|
||||
readonly property real windowWidth: alignedWidth + contentX + shadowPad
|
||||
readonly property real windowHeight: alignedHeight + contentY + shadowPad
|
||||
|
||||
onAlignedXChanged: _kickBlurCommit()
|
||||
onAlignedYChanged: _kickBlurCommit()
|
||||
onAlignedWidthChanged: _kickBlurCommit()
|
||||
onAlignedHeightChanged: _kickBlurCommit()
|
||||
onContentVisibleChanged: _kickBlurCommit()
|
||||
|
||||
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property bool useBackgroundDarken: !FrameTransitionState.effectiveFrameEnabled && SettingsData.modalDarkenBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
||||
@@ -119,11 +113,6 @@ Item {
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
function _kickBlurCommit() {
|
||||
if (typeof launcherWindow.update === "function")
|
||||
launcherWindow.update();
|
||||
}
|
||||
|
||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||
_pendingQuery = query || "";
|
||||
_pendingMode = mode || "";
|
||||
@@ -377,13 +366,11 @@ Item {
|
||||
WindowBlur {
|
||||
targetWindow: launcherWindow
|
||||
readonly property real s: Math.min(1, modalContainer.publishedScale)
|
||||
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
||||
readonly property real visibleScale: s * op
|
||||
// Blur tracks the surface's scaled rect
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5
|
||||
blurWidth: contentVisible ? modalContainer.width * visibleScale : 0
|
||||
blurHeight: contentVisible ? modalContainer.height * visibleScale : 0
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
|
||||
blurWidth: contentVisible ? modalContainer.width * s : 0
|
||||
blurHeight: contentVisible ? modalContainer.height * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
@@ -473,8 +460,6 @@ Item {
|
||||
opacity: contentVisible ? 1 : 0
|
||||
scale: contentVisible ? 1 : 0.96
|
||||
transformOrigin: Item.Center
|
||||
onOpacityChanged: root._kickBlurCommit()
|
||||
onPublishedScaleChanged: root._kickBlurCommit()
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
|
||||
@@ -54,7 +54,14 @@ FocusScope {
|
||||
return;
|
||||
editingApp = app;
|
||||
editAppId = app.id || app.execString || app.exec || "";
|
||||
var existing = SessionData.getAppOverride(editAppId);
|
||||
editNameField.text = existing?.name || "";
|
||||
editIconField.text = existing?.icon || "";
|
||||
editCommentField.text = existing?.comment || "";
|
||||
editEnvVarsField.text = existing?.envVars || "";
|
||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
editMode = true;
|
||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function closeEditMode() {
|
||||
@@ -64,6 +71,27 @@ FocusScope {
|
||||
Qt.callLater(() => searchField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function saveAppOverride() {
|
||||
var override = {};
|
||||
if (editNameField.text.trim())
|
||||
override.name = editNameField.text.trim();
|
||||
if (editIconField.text.trim())
|
||||
override.icon = editIconField.text.trim();
|
||||
if (editCommentField.text.trim())
|
||||
override.comment = editCommentField.text.trim();
|
||||
if (editEnvVarsField.text.trim())
|
||||
override.envVars = editEnvVarsField.text.trim();
|
||||
if (editExtraFlagsField.text.trim())
|
||||
override.extraFlags = editExtraFlagsField.text.trim();
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function resetAppOverride() {
|
||||
SessionData.clearAppOverride(editAppId);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function showContextMenu(item, x, y, fromKeyboard) {
|
||||
if (!item)
|
||||
return;
|
||||
@@ -789,21 +817,291 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: editLoader
|
||||
FocusScope {
|
||||
id: editView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
active: root.editMode
|
||||
visible: active
|
||||
focus: root.editMode
|
||||
visible: editMode
|
||||
focus: editMode
|
||||
|
||||
sourceComponent: AppEditView {
|
||||
focus: true
|
||||
editingApp: root.editingApp
|
||||
editAppId: root.editAppId
|
||||
onCloseRequested: root.closeEditMode()
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
closeEditMode();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
|
||||
saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: item.loadOverride()
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: backButtonArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: closeEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
width: 40
|
||||
height: 40
|
||||
source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Edit App")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: editingApp?.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
}
|
||||
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - y - buttonsRow.height - Theme.spacingM
|
||||
contentHeight: editFieldsColumn.height
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: editFieldsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Name")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editNameField
|
||||
width: parent.width
|
||||
placeholderText: editingApp?.name || ""
|
||||
keyNavigationTab: editIconField
|
||||
keyNavigationBacktab: editExtraFlagsField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Icon")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editIconField
|
||||
width: parent.width
|
||||
placeholderText: editingApp?.icon || ""
|
||||
keyNavigationTab: editCommentField
|
||||
keyNavigationBacktab: editNameField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Description")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editCommentField
|
||||
width: parent.width
|
||||
placeholderText: editingApp?.comment || ""
|
||||
keyNavigationTab: editEnvVarsField
|
||||
keyNavigationBacktab: editIconField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Environment Variables")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "KEY=value KEY2=value2"
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editEnvVarsField
|
||||
width: parent.width
|
||||
placeholderText: "VAR=value"
|
||||
keyNavigationTab: editExtraFlagsField
|
||||
keyNavigationBacktab: editCommentField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Extra Arguments")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editExtraFlagsField
|
||||
width: parent.width
|
||||
placeholderText: "--flag --option=value"
|
||||
keyNavigationTab: editNameField
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonsRow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
id: resetButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
visible: SessionData.getAppOverride(editAppId) !== null
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reset")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.error
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resetButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: resetAppOverride()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cancelButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: closeEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: saveButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: saveButtonArea.containsMouse ? Theme.withAlpha(Theme.primary, 0.9) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: saveAppOverride()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ Rectangle {
|
||||
return item.icon || "";
|
||||
}
|
||||
}
|
||||
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/")
|
||||
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
|
||||
|
||||
width: parent?.width ?? 200
|
||||
height: 52
|
||||
|
||||
@@ -241,124 +241,107 @@ Item {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string rowType: modelData?.type ?? ""
|
||||
|
||||
width: mainListView.width
|
||||
height: modelData?.height ?? 52
|
||||
|
||||
Loader {
|
||||
SectionHeader {
|
||||
anchors.fill: parent
|
||||
active: delegateRoot.rowType === "header"
|
||||
visible: active
|
||||
sourceComponent: SectionHeader {
|
||||
section: delegateRoot.modelData?.section ?? null
|
||||
controller: root.controller
|
||||
viewMode: {
|
||||
var vt = root.controller?.viewModeVersion ?? 0;
|
||||
void (vt);
|
||||
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
|
||||
}
|
||||
canChangeViewMode: {
|
||||
var vt = root.controller?.viewModeVersion ?? 0;
|
||||
void (vt);
|
||||
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
|
||||
}
|
||||
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
visible: delegateRoot.modelData?.type === "header"
|
||||
section: delegateRoot.modelData?.section ?? null
|
||||
controller: root.controller
|
||||
viewMode: {
|
||||
var vt = root.controller?.viewModeVersion ?? 0;
|
||||
void (vt);
|
||||
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
|
||||
}
|
||||
canChangeViewMode: {
|
||||
var vt = root.controller?.viewModeVersion ?? 0;
|
||||
void (vt);
|
||||
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
|
||||
}
|
||||
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
}
|
||||
|
||||
Loader {
|
||||
ResultItem {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 2
|
||||
anchors.bottomMargin: 2
|
||||
active: delegateRoot.rowType === "list_item"
|
||||
visible: active
|
||||
sourceComponent: ResultItem {
|
||||
item: delegateRoot.modelData?.item ?? null
|
||||
isSelected: (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: delegateRoot.modelData?.flatIndex ?? -1
|
||||
visible: delegateRoot.modelData?.type === "list_item"
|
||||
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
|
||||
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
|
||||
|
||||
onClicked: {
|
||||
if (root.controller && delegateRoot.modelData?.item) {
|
||||
root.controller.executeItem(delegateRoot.modelData.item);
|
||||
}
|
||||
onClicked: {
|
||||
if (root.controller && delegateRoot.modelData?.item) {
|
||||
root.controller.executeItem(delegateRoot.modelData.item);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
|
||||
}
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Row {
|
||||
id: gridRowContent
|
||||
anchors.fill: parent
|
||||
active: delegateRoot.rowType === "grid_row"
|
||||
visible: active
|
||||
sourceComponent: Row {
|
||||
Repeater {
|
||||
model: delegateRoot.modelData?.items ?? []
|
||||
visible: delegateRoot.modelData?.type === "grid_row"
|
||||
|
||||
Item {
|
||||
id: gridCellDelegate
|
||||
required property var modelData
|
||||
required property int index
|
||||
Repeater {
|
||||
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
|
||||
|
||||
readonly property bool isTile: delegateRoot.modelData?.viewMode === "tile"
|
||||
readonly property real cellWidth: isTile ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
|
||||
Item {
|
||||
id: gridCellDelegate
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: cellWidth
|
||||
height: delegateRoot.height
|
||||
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
|
||||
|
||||
Loader {
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: gridCellDelegate.isTile ? tileCellComponent : gridCellComponent
|
||||
width: cellWidth
|
||||
height: delegateRoot.height
|
||||
|
||||
Component {
|
||||
id: gridCellComponent
|
||||
GridItem {
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
anchors.centerIn: parent
|
||||
visible: delegateRoot.modelData?.viewMode === "grid"
|
||||
item: gridCellDelegate.modelData?.item ?? null
|
||||
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
|
||||
|
||||
GridItem {
|
||||
item: gridCellDelegate.modelData?.item ?? null
|
||||
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
|
||||
|
||||
onClicked: {
|
||||
if (root.controller && gridCellDelegate.modelData?.item) {
|
||||
root.controller.executeItem(gridCellDelegate.modelData.item);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
if (root.controller && gridCellDelegate.modelData?.item) {
|
||||
root.controller.executeItem(gridCellDelegate.modelData.item);
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: tileCellComponent
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
TileItem {
|
||||
item: gridCellDelegate.modelData?.item ?? null
|
||||
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
|
||||
TileItem {
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
anchors.centerIn: parent
|
||||
visible: delegateRoot.modelData?.viewMode === "tile"
|
||||
item: gridCellDelegate.modelData?.item ?? null
|
||||
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
|
||||
|
||||
onClicked: {
|
||||
if (root.controller && gridCellDelegate.modelData?.item) {
|
||||
root.controller.executeItem(gridCellDelegate.modelData.item);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
if (root.controller && gridCellDelegate.modelData?.item) {
|
||||
root.controller.executeItem(gridCellDelegate.modelData.item);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,9 @@ Rectangle {
|
||||
return "file://" + raw;
|
||||
return raw;
|
||||
}
|
||||
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/")
|
||||
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
|
||||
readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
|
||||
readonly property bool previewAnimated: previewSource.toLowerCase().indexOf(".gif") >= 0
|
||||
|
||||
readonly property string typeLabel: {
|
||||
if (!item)
|
||||
@@ -283,10 +284,16 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
source: root.previewSource
|
||||
asynchronous: true
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: !root.hasClipboardPreview
|
||||
visible: !root.hasClipboardPreview && !root.previewAnimated
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
anchors.fill: parent
|
||||
source: root.previewSource
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
playing: visible
|
||||
visible: !root.hasClipboardPreview && root.previewAnimated
|
||||
}
|
||||
|
||||
ClipboardLauncherPreview {
|
||||
|
||||
@@ -165,9 +165,6 @@ Rectangle {
|
||||
anchors.margins: root.hasScreencopy ? 4 : 0
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: root.item?.data?.attribution || ""
|
||||
asynchronous: true
|
||||
sourceSize.width: 80
|
||||
sourceSize.height: 80
|
||||
mipmap: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
@@ -98,25 +98,25 @@ StyledRect {
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
property bool _thumbGenAttempted: false
|
||||
|
||||
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||
onVideoThumbnailPathChanged: {
|
||||
_thumbGenAttempted = false;
|
||||
_videoThumb = videoThumbnailPath;
|
||||
}
|
||||
|
||||
function generateVideoThumbnail() {
|
||||
if (_thumbGenAttempted)
|
||||
return;
|
||||
_thumbGenAttempted = true;
|
||||
_videoThumb = "";
|
||||
if (!videoThumbnailPath)
|
||||
return;
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s " + _thumbnailPx + " -f";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, delegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
const size = _thumbnailPx;
|
||||
const fp = delegateRoot.filePath;
|
||||
Paths.mkdir(thumbDir);
|
||||
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0) {
|
||||
_videoThumb = thumbPath;
|
||||
} else {
|
||||
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,52 +160,62 @@ StyledRect {
|
||||
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
ClippingRectangle {
|
||||
Image {
|
||||
id: gridPreviewImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: gridPreviewImage
|
||||
anchors.fill: parent
|
||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
||||
property int weExtIndex: 0
|
||||
property string imagePath: {
|
||||
if (weMode && delegateRoot.fileIsDir)
|
||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
onStatusChanged: {
|
||||
if (status !== Image.Error)
|
||||
return;
|
||||
if (weMode && delegateRoot.fileIsDir) {
|
||||
if (weExtIndex < weExtensions.length - 1) {
|
||||
weExtIndex++;
|
||||
} else {
|
||||
imagePath = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_videoThumb)
|
||||
generateVideoThumbnail();
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
|
||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
||||
property int weExtIndex: 0
|
||||
property string imagePath: {
|
||||
if (weMode && delegateRoot.fileIsDir)
|
||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
||||
if (!delegateRoot.fileIsDir && isImage)
|
||||
return delegateRoot.filePath;
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
onStatusChanged: {
|
||||
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
|
||||
if (weExtIndex < weExtensions.length - 1) {
|
||||
weExtIndex++;
|
||||
} else {
|
||||
imagePath = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
asynchronous: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: gridPreviewImage
|
||||
maskEnabled: true
|
||||
maskSource: gridImageMask
|
||||
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && (isImage || isVideo)) || (weMode && delegateRoot.fileIsDir))
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gridImageMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
visible: !delegateRoot.fileIsDir && isImage
|
||||
radius: Theme.cornerRadius
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
@@ -95,25 +95,23 @@ StyledRect {
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
property bool _thumbGenAttempted: false
|
||||
|
||||
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||
onVideoThumbnailPathChanged: {
|
||||
_thumbGenAttempted = false;
|
||||
_videoThumb = videoThumbnailPath;
|
||||
}
|
||||
|
||||
function generateVideoThumbnail() {
|
||||
if (_thumbGenAttempted)
|
||||
return;
|
||||
_thumbGenAttempted = true;
|
||||
_videoThumb = "";
|
||||
if (!videoThumbnailPath)
|
||||
return;
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/normal";
|
||||
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s 128 -f";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, listDelegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
const fp = listDelegateRoot.filePath;
|
||||
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
|
||||
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0) {
|
||||
_videoThumb = thumbPath;
|
||||
} else {
|
||||
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,33 +165,46 @@ StyledRect {
|
||||
height: 28
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
ClippingRectangle {
|
||||
Image {
|
||||
id: listPreviewImage
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: listPreviewImage
|
||||
anchors.fill: parent
|
||||
property string imagePath: _videoThumb
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: 32
|
||||
sourceSize.height: 32
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error && _videoThumb)
|
||||
generateVideoThumbnail();
|
||||
}
|
||||
property string imagePath: {
|
||||
if (!listDelegateRoot.fileIsDir && isImage)
|
||||
return listDelegateRoot.filePath;
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: 32
|
||||
sourceSize.height: 32
|
||||
asynchronous: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: listPreviewImage
|
||||
maskEnabled: true
|
||||
maskSource: listImageMask
|
||||
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && (isImage || isVideo)
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: listImageMask
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
visible: !listDelegateRoot.fileIsDir && isImage
|
||||
radius: Theme.cornerRadius
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -591,11 +591,24 @@ DankModal {
|
||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
ClippingRectangle {
|
||||
Rectangle {
|
||||
id: gridProgressMask
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: gridButtonRect.isHolding
|
||||
layer.enabled: gridButtonRect.isHolding
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: gridProgressMask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
@@ -716,11 +729,24 @@ DankModal {
|
||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
ClippingRectangle {
|
||||
Rectangle {
|
||||
id: listProgressMask
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: listButtonRect.isHolding
|
||||
layer.enabled: listButtonRect.isHolding
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: listProgressMask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -108,48 +108,16 @@ Variants {
|
||||
function invalidate() {
|
||||
_settleFrames = 3;
|
||||
backingWindow?.update();
|
||||
if (!_wedgeBounced)
|
||||
wedgeWatchdog.restart();
|
||||
}
|
||||
|
||||
onRenderActiveChanged: invalidate()
|
||||
onBackingWindowChanged: invalidate()
|
||||
|
||||
// Same wedge recovery as WallpaperBackground
|
||||
property bool _wedgeBounced: false
|
||||
|
||||
Timer {
|
||||
id: wedgeWatchdog
|
||||
interval: 3000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.backingWindow || !blurWallpaperWindow.visible || IdleService.isShellLocked)
|
||||
return;
|
||||
log.warn("no frame swapped on", modelData.name, "since last invalidate, re-attaching surface");
|
||||
root._wedgeBounced = true;
|
||||
surfaceReattach.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: surfaceReattach
|
||||
interval: 0
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
blurWallpaperWindow.visible = false;
|
||||
Qt.callLater(() => {
|
||||
blurWallpaperWindow.visible = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backingWindow
|
||||
function onFrameSwapped() {
|
||||
if (root._settleFrames > 0)
|
||||
root._settleFrames--;
|
||||
root._wedgeBounced = false;
|
||||
wedgeWatchdog.stop();
|
||||
}
|
||||
function onVisibleChanged() {
|
||||
root.invalidate();
|
||||
@@ -174,29 +142,10 @@ Variants {
|
||||
function onWallpaperFillModeChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
function onEffectiveWallpaperBackgroundColorChanged() {
|
||||
function onWallpaperBackgroundColorModeChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onMonitorWallpaperFillModesChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
function onPerMonitorWallpaperChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
// Theme changes repaint DankBackdrop but nothing else wakes the render loop
|
||||
Connections {
|
||||
target: Theme
|
||||
enabled: root.isColorSource || currentWallpaper.status === Image.Error
|
||||
function onPrimaryChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
function onBackgroundChanged() {
|
||||
function onWallpaperBackgroundCustomColorChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
@@ -221,7 +170,6 @@ Variants {
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
invalidate();
|
||||
if (!source || source.startsWith("#")) {
|
||||
setWallpaperImmediate("");
|
||||
return;
|
||||
@@ -242,7 +190,6 @@ Variants {
|
||||
}
|
||||
|
||||
function setWallpaperImmediate(newSource) {
|
||||
transitionDelayTimer.stop();
|
||||
transitionAnimation.stop();
|
||||
root.transitionProgress = 0.0;
|
||||
root.effectActive = false;
|
||||
|
||||
@@ -423,14 +423,12 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
// targetValue-based direction: `active` can be stale when the behavior triggers
|
||||
Behavior on height {
|
||||
id: detailHeightBehavior
|
||||
enabled: true
|
||||
NumberAnimation {
|
||||
readonly property bool growing: (detailHeightBehavior.targetValue ?? 0) >= detailHost.height
|
||||
duration: Theme.variantDuration(Theme.popoutAnimationDuration, growing)
|
||||
duration: Theme.variantDuration(Theme.popoutAnimationDuration, detailHost.active)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: growing ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
easing.bezierCurve: detailHost.active ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -941,16 +941,12 @@ Item {
|
||||
section: topBarContent.getWidgetSection(parent)
|
||||
parentScreen: barWindow.screen
|
||||
onClicked: {
|
||||
if (!powerMenuModalLoader)
|
||||
return;
|
||||
powerMenuModalLoader.active = true;
|
||||
if (!powerMenuModalLoader.item)
|
||||
return;
|
||||
if (powerMenuModalLoader.item.shouldBeVisible) {
|
||||
powerMenuModalLoader.item.close();
|
||||
return;
|
||||
if (powerMenuModalLoader) {
|
||||
powerMenuModalLoader.active = true;
|
||||
if (powerMenuModalLoader.item) {
|
||||
powerMenuModalLoader.item.openCentered();
|
||||
}
|
||||
}
|
||||
powerMenuModalLoader.item.openCentered();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1105,13 +1101,12 @@ Item {
|
||||
|
||||
onClockClicked: {
|
||||
const section = topBarContent.getWidgetSection(parent) || "center";
|
||||
const tabIndex = SettingsData.dashTabIndexForId("overview");
|
||||
topBarContent.openWidgetPopout({
|
||||
loader: dankDashPopoutLoader,
|
||||
widgetItem: clockWidget,
|
||||
section,
|
||||
tabIndex,
|
||||
triggerSource: topBarContent._dashTriggerSource(section, tabIndex),
|
||||
tabIndex: 0,
|
||||
triggerSource: topBarContent._dashTriggerSource(section, 0),
|
||||
mode: "click",
|
||||
useCenterSection: true,
|
||||
setTriggerScreen: true
|
||||
@@ -1134,13 +1129,12 @@ Item {
|
||||
parentScreen: barWindow.screen
|
||||
onClicked: {
|
||||
const section = topBarContent.getWidgetSection(parent) || "center";
|
||||
const tabIndex = SettingsData.dashTabIndexForId("media");
|
||||
topBarContent.openWidgetPopout({
|
||||
loader: dankDashPopoutLoader,
|
||||
widgetItem: mediaWidget,
|
||||
section,
|
||||
tabIndex,
|
||||
triggerSource: topBarContent._dashTriggerSource(section, tabIndex),
|
||||
tabIndex: 1,
|
||||
triggerSource: topBarContent._dashTriggerSource(section, 1),
|
||||
mode: "click",
|
||||
useCenterSection: true,
|
||||
setTriggerScreen: true
|
||||
@@ -1162,13 +1156,12 @@ Item {
|
||||
parentScreen: barWindow.screen
|
||||
onClicked: {
|
||||
const section = topBarContent.getWidgetSection(parent) || "center";
|
||||
const tabIndex = SettingsData.dashTabIndexForId("weather");
|
||||
topBarContent.openWidgetPopout({
|
||||
loader: dankDashPopoutLoader,
|
||||
widgetItem: weatherWidget,
|
||||
section,
|
||||
tabIndex,
|
||||
triggerSource: topBarContent._dashTriggerSource(section, tabIndex),
|
||||
tabIndex: 3,
|
||||
triggerSource: topBarContent._dashTriggerSource(section, 3),
|
||||
mode: "click",
|
||||
useCenterSection: true,
|
||||
setTriggerScreen: true
|
||||
|
||||
@@ -736,7 +736,7 @@ Item {
|
||||
case "music":
|
||||
case "weather":
|
||||
{
|
||||
const tabIndex = SettingsData.dashTabIndexForId(widgetId === "clock" ? "overview" : (widgetId === "music" ? "media" : "weather"));
|
||||
const tabIndex = widgetId === "clock" ? 0 : (widgetId === "music" ? 1 : 3);
|
||||
return barContent.openWidgetPopout(Object.assign({}, base, {
|
||||
loader,
|
||||
tabIndex,
|
||||
|
||||
@@ -240,7 +240,7 @@ PanelWindow {
|
||||
id: barBlur
|
||||
visible: false
|
||||
|
||||
readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1
|
||||
readonly property bool barHasTransparency: barWindow._backgroundAlpha < 1
|
||||
|
||||
function rebuild() {
|
||||
teardown();
|
||||
|
||||
@@ -7,24 +7,15 @@ Item {
|
||||
id: root
|
||||
|
||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
readonly property bool isPlaying: activePlayer !== null && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
readonly property bool live: visible && isPlaying
|
||||
readonly property bool hasActiveMedia: activePlayer !== null
|
||||
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
width: 20
|
||||
height: Theme.iconSize
|
||||
|
||||
readonly property real maxBarHeight: Theme.iconSize - 2
|
||||
readonly property real minBarHeight: 3
|
||||
|
||||
onLiveChanged: {
|
||||
if (!live) {
|
||||
bars.bandsA = Qt.vector4d(0, 0, 0, 0);
|
||||
bars.bandsB = Qt.vector2d(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.live
|
||||
active: isPlaying
|
||||
|
||||
sourceComponent: Component {
|
||||
Ref {
|
||||
service: CavaService
|
||||
@@ -32,8 +23,15 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real maxBarHeight: Theme.iconSize - 2
|
||||
readonly property real minBarHeight: 3
|
||||
readonly property real heightRange: maxBarHeight - minBarHeight
|
||||
property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight]
|
||||
|
||||
Timer {
|
||||
running: !CavaService.cavaAvailable && root.live
|
||||
id: fallbackTimer
|
||||
|
||||
running: !CavaService.cavaAvailable && isPlaying
|
||||
interval: 500
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
@@ -43,32 +41,54 @@ Item {
|
||||
|
||||
Connections {
|
||||
target: CavaService
|
||||
enabled: root.live
|
||||
function onValuesChanged() {
|
||||
const v = CavaService.values;
|
||||
if (v.length < 6)
|
||||
if (!root.isPlaying) {
|
||||
root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight];
|
||||
return;
|
||||
const n = i => {
|
||||
const x = v[i];
|
||||
return x <= 0 ? 0 : x >= 100 ? 1 : Math.sqrt(x * 0.01);
|
||||
};
|
||||
bars.bandsA = Qt.vector4d(n(0), n(1), n(2), n(3));
|
||||
bars.bandsB = Qt.vector2d(n(4), n(5));
|
||||
}
|
||||
|
||||
const newHeights = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (CavaService.values.length <= i) {
|
||||
newHeights.push(root.minBarHeight);
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawLevel = CavaService.values[i];
|
||||
if (rawLevel <= 0) {
|
||||
newHeights.push(root.minBarHeight);
|
||||
} else if (rawLevel >= 100) {
|
||||
newHeights.push(root.maxBarHeight);
|
||||
} else {
|
||||
newHeights.push(root.minBarHeight + Math.sqrt(rawLevel * 0.01) * root.heightRange);
|
||||
}
|
||||
}
|
||||
root.barHeights = newHeights;
|
||||
}
|
||||
}
|
||||
|
||||
ShaderEffect {
|
||||
id: bars
|
||||
anchors.fill: parent
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 1.5
|
||||
|
||||
property real widthPx: width
|
||||
property real heightPx: height
|
||||
property real minH: root.minBarHeight
|
||||
property real maxH: root.maxBarHeight
|
||||
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
|
||||
property vector2d bandsB: Qt.vector2d(0, 0)
|
||||
property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a)
|
||||
Repeater {
|
||||
model: 6
|
||||
|
||||
fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/viz_bars.frag.qsb")
|
||||
Rectangle {
|
||||
width: 2
|
||||
height: root.barHeights[index]
|
||||
radius: 1.5
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on height {
|
||||
enabled: root.isPlaying && !CavaService.cavaAvailable
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
@@ -233,19 +234,16 @@ BasePill {
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
Row {
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: !root.isVerticalOrientation
|
||||
|
||||
readonly property real iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
|
||||
IconImage {
|
||||
id: horizontalAppIcon
|
||||
width: contentRow.iconSize
|
||||
height: contentRow.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
Layout.preferredHeight: Layout.preferredWidth
|
||||
visible: root.showIcon && activeWindow && status === Image.Ready
|
||||
source: {
|
||||
if (!activeWindow || !activeWindow.appId)
|
||||
@@ -266,10 +264,8 @@ BasePill {
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: horizontalSteamIcon
|
||||
width: contentRow.iconSize
|
||||
size: contentRow.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
size: Layout.preferredWidth
|
||||
name: "sports_esports"
|
||||
color: Theme.widgetTextColor
|
||||
visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
|
||||
@@ -284,11 +280,9 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
wrapMode: Text.NoWrap
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
width: Math.min(implicitWidth, compactMode ? 80 : 180)
|
||||
Layout.maximumWidth: compactMode ? 80 : 180
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
@@ -297,7 +291,6 @@ BasePill {
|
||||
text: compactMode ? "" : "•"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.outlineButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !compactMode && appText.text && titleText.text
|
||||
}
|
||||
|
||||
@@ -325,24 +318,9 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
wrapMode: Text.NoWrap
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
width: {
|
||||
const sp = contentRow.spacing;
|
||||
let used = 0;
|
||||
if (horizontalAppIcon.visible)
|
||||
used += horizontalAppIcon.width + sp;
|
||||
else if (horizontalSteamIcon.visible)
|
||||
used += horizontalSteamIcon.width + sp;
|
||||
if (appText.visible)
|
||||
used += appText.width + sp;
|
||||
if (appSeparator.visible)
|
||||
used += appSeparator.width + sp;
|
||||
const budget = root.maxWidth - root.horizontalPadding * 2 - used;
|
||||
return Math.min(implicitWidth, Math.max(0, budget));
|
||||
}
|
||||
Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
@@ -1047,9 +1048,12 @@ BasePill {
|
||||
WlrLayershell.namespace: "dms:tray-overflow-menu"
|
||||
color: "transparent"
|
||||
|
||||
DankFocusGrab {
|
||||
HyprlandFocusGrab {
|
||||
windows: [overflowMenu].concat(KeyboardFocus.barWindows)
|
||||
wanted: root.useOverflowPopup && KeyboardFocus.wantsGrab(root.menuOpen, null)
|
||||
active: root.useOverflowPopup && KeyboardFocus.wantsGrab(root.menuOpen, null)
|
||||
|
||||
property var restoreToplevel: null
|
||||
onActiveChanged: restoreToplevel = active ? KeyboardFocus.captureActiveToplevel() : KeyboardFocus.restoreToplevel(restoreToplevel)
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -1597,9 +1601,12 @@ BasePill {
|
||||
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(menuRoot.showMenu, null)
|
||||
color: "transparent"
|
||||
|
||||
DankFocusGrab {
|
||||
HyprlandFocusGrab {
|
||||
windows: [menuWindow].concat(KeyboardFocus.barWindows)
|
||||
wanted: KeyboardFocus.wantsGrab(menuRoot.showMenu, null)
|
||||
active: KeyboardFocus.wantsGrab(menuRoot.showMenu, null)
|
||||
|
||||
property var restoreToplevel: null
|
||||
onActiveChanged: restoreToplevel = active ? KeyboardFocus.captureActiveToplevel() : KeyboardFocus.restoreToplevel(restoreToplevel)
|
||||
}
|
||||
|
||||
anchors {
|
||||
|
||||
@@ -107,14 +107,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onCompositorChanged() {
|
||||
root._placeholderPool = [];
|
||||
root._hyprSlotPool = {};
|
||||
}
|
||||
}
|
||||
|
||||
property var currentWorkspace: {
|
||||
if (useExtWorkspace)
|
||||
return getExtWorkspaceActiveWorkspace();
|
||||
@@ -153,7 +145,8 @@ Item {
|
||||
baseList = getNiriWorkspaces();
|
||||
break;
|
||||
case "hyprland":
|
||||
return hyprlandSlotList(getHyprlandWorkspaces());
|
||||
baseList = getHyprlandWorkspaces();
|
||||
break;
|
||||
case "mango":
|
||||
if (root.mangoOverviewActive)
|
||||
return [];
|
||||
@@ -182,7 +175,7 @@ Item {
|
||||
function mapWorkspace(ws) {
|
||||
return {
|
||||
"num": ws.number,
|
||||
"name": stripSwayWorkspaceNumber(ws.number, ws.name),
|
||||
"name": ws.name,
|
||||
"focused": ws.focused,
|
||||
"active": ws.active,
|
||||
"urgent": ws.urgent,
|
||||
@@ -202,18 +195,6 @@ Item {
|
||||
];
|
||||
}
|
||||
|
||||
// sway/scroll fold `<num>:<name>` into the name field (num 1 → name "1:test"); drop the redundant prefix so the index option controls it
|
||||
function stripSwayWorkspaceNumber(num, name) {
|
||||
if (num === undefined || num === -1)
|
||||
return name;
|
||||
if (typeof name !== "string")
|
||||
return name;
|
||||
const prefix = num + ":";
|
||||
if (!name.startsWith(prefix))
|
||||
return name;
|
||||
return name.slice(prefix.length);
|
||||
}
|
||||
|
||||
// Numbered workspaces first in ascending order; purely-named workspaces (sway reports num -1) after, by name
|
||||
function swayWorkspaceOrder(a, b) {
|
||||
const keyA = a.num === -1 ? Number.MAX_SAFE_INTEGER : a.num;
|
||||
@@ -445,84 +426,41 @@ Item {
|
||||
return Object.values(byApp);
|
||||
}
|
||||
|
||||
function _makePlaceholder() {
|
||||
if (useExtWorkspace)
|
||||
return {
|
||||
function padWorkspaces(list) {
|
||||
const padded = list.slice();
|
||||
let placeholder;
|
||||
if (useExtWorkspace) {
|
||||
placeholder = {
|
||||
"id": "",
|
||||
"name": "",
|
||||
"active": false,
|
||||
"_placeholder": true
|
||||
};
|
||||
if (CompositorService.isNiri)
|
||||
return {
|
||||
} else if (CompositorService.isNiri) {
|
||||
placeholder = {
|
||||
"id": -1,
|
||||
"idx": -1,
|
||||
"name": ""
|
||||
};
|
||||
if (CompositorService.isHyprland)
|
||||
return {
|
||||
} else if (CompositorService.isHyprland) {
|
||||
placeholder = {
|
||||
"id": -1,
|
||||
"name": ""
|
||||
};
|
||||
if (root.isMango)
|
||||
return {
|
||||
} else if (root.isMango) {
|
||||
placeholder = {
|
||||
"tag": -1
|
||||
};
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return {
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
placeholder = {
|
||||
"num": -1,
|
||||
"_placeholder": true
|
||||
};
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Hyprland creates/destroys workspaces on empty enter/leave; slots keyed by id keep delegate identity so pills animate instead of popping
|
||||
property var _hyprSlotPool: ({})
|
||||
|
||||
Component {
|
||||
id: hyprSlotComponent
|
||||
|
||||
QtObject {
|
||||
property var ws: null
|
||||
readonly property var id: ws ? ws.id : -1
|
||||
readonly property string name: ws?.name ?? ""
|
||||
readonly property bool urgent: ws?.urgent ?? false
|
||||
} else {
|
||||
placeholder = -1;
|
||||
}
|
||||
}
|
||||
|
||||
function _hyprSlot(key, ws) {
|
||||
let slot = _hyprSlotPool[key];
|
||||
if (!slot) {
|
||||
slot = hyprSlotComponent.createObject(root);
|
||||
_hyprSlotPool[key] = slot;
|
||||
}
|
||||
if (slot.ws !== ws)
|
||||
slot.ws = ws;
|
||||
return slot;
|
||||
}
|
||||
|
||||
function hyprlandSlotList(raw) {
|
||||
const slots = raw.map(ws => _hyprSlot(ws.id > 0 ? ws.id : "name:" + (ws.name ?? ""), ws));
|
||||
if (!SettingsData.showWorkspacePadding)
|
||||
return slots;
|
||||
// pad past the highest real id so a placeholder becomes that workspace's slot once created
|
||||
let nextId = raw.reduce((max, ws) => Math.max(max, ws.id ?? 0), 0);
|
||||
while (slots.length < 3)
|
||||
slots.push(_hyprSlot(++nextId, null));
|
||||
return slots;
|
||||
}
|
||||
|
||||
// Stable placeholder instances so ScriptModel (identity-diffed) reuses padding delegates instead of recreating them on workspace churn
|
||||
property var _placeholderPool: []
|
||||
|
||||
function padWorkspaces(list) {
|
||||
const padded = list.slice();
|
||||
let slot = 0;
|
||||
while (padded.length < 3) {
|
||||
if (root._placeholderPool.length <= slot)
|
||||
root._placeholderPool.push(root._makePlaceholder());
|
||||
padded.push(root._placeholderPool[slot]);
|
||||
slot++;
|
||||
padded.push(placeholder);
|
||||
}
|
||||
return padded;
|
||||
}
|
||||
@@ -700,7 +638,7 @@ Item {
|
||||
NiriService.switchToWorkspace(data.id);
|
||||
break;
|
||||
case "hyprland":
|
||||
if (data.id && data.id !== -1) {
|
||||
if (data.id) {
|
||||
HyprlandService.focusWorkspace(hyprlandWorkspaceSelector(data));
|
||||
}
|
||||
break;
|
||||
@@ -725,7 +663,7 @@ Item {
|
||||
|
||||
for (let i = 0; i < workspaceRepeater.count; i++) {
|
||||
const item = workspaceRepeater.itemAt(i);
|
||||
if (!item || item.isPlaceholder)
|
||||
if (!item)
|
||||
continue;
|
||||
const center = item.mapToItem(root, item.width / 2, item.height / 2);
|
||||
const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x);
|
||||
|
||||
@@ -240,10 +240,8 @@ DankPopout {
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (!root.shouldBeVisible)
|
||||
return;
|
||||
mainContainer.forceActiveFocus();
|
||||
tabBar.snapIndicator();
|
||||
if (root.shouldBeVisible)
|
||||
mainContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +411,6 @@ DankPopout {
|
||||
anchors.fill: parent
|
||||
active: root.currentTabId === "media"
|
||||
visible: active
|
||||
asynchronous: true
|
||||
sourceComponent: Component {
|
||||
MediaPlayerTab {
|
||||
targetScreen: root.screen
|
||||
@@ -450,7 +447,6 @@ DankPopout {
|
||||
anchors.fill: parent
|
||||
active: root.currentTabId === "wallpaper"
|
||||
visible: active
|
||||
asynchronous: true
|
||||
sourceComponent: Component {
|
||||
WallpaperTab {
|
||||
active: true
|
||||
@@ -462,18 +458,11 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
size: 40
|
||||
visible: (wallpaperLoader.active && wallpaperLoader.status === Loader.Loading) || (mediaLoader.active && mediaLoader.status === Loader.Loading) || (weatherLoader.active && weatherLoader.status === Loader.Loading)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: weatherLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentTabId === "weather"
|
||||
visible: active
|
||||
asynchronous: true
|
||||
sourceComponent: Component {
|
||||
WeatherTab {}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@ Item {
|
||||
|
||||
property int __panelHoverCount: 0
|
||||
|
||||
readonly property bool overlayBlurActive: dropdownBlur.active
|
||||
|
||||
onDropdownTypeChanged: {
|
||||
if (dropdownType === 0) {
|
||||
__panelHoverCount = 0;
|
||||
@@ -77,10 +75,8 @@ Item {
|
||||
}
|
||||
|
||||
WindowBlur {
|
||||
id: dropdownBlur
|
||||
targetWindow: root.targetWindow
|
||||
readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0
|
||||
blurEnabled: active && Theme.connectedSurfaceBlurEnabled
|
||||
readonly property real s: root.__activePanel ? Math.min(1, root.__activePanel.scale) : 1
|
||||
blurX: root.__activePanel ? root.__activePanel.x + root.__activePanel.width * (1 - s) * 0.5 : 0
|
||||
blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0
|
||||
|
||||
@@ -2,7 +2,6 @@ import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -25,11 +24,6 @@ Item {
|
||||
property string section: ""
|
||||
property int barPosition: SettingsData.Position.Top
|
||||
|
||||
readonly property color accent: MediaAccentService.accent
|
||||
readonly property color onAccent: MediaAccentService.onAccent
|
||||
readonly property color accentHover: MediaAccentService.accentHover
|
||||
readonly property color accentPressed: MediaAccentService.accentPressed
|
||||
|
||||
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
|
||||
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||
@@ -76,7 +70,7 @@ Item {
|
||||
property bool _switchHold: false
|
||||
Timer {
|
||||
id: _switchHoldTimer
|
||||
interval: 1500
|
||||
interval: 650
|
||||
repeat: false
|
||||
onTriggered: _switchHold = false
|
||||
}
|
||||
@@ -84,8 +78,7 @@ Item {
|
||||
onActivePlayerChanged: {
|
||||
if (!activePlayer) {
|
||||
isSwitching = false;
|
||||
_switchHold = true;
|
||||
_switchHoldTimer.restart();
|
||||
_switchHold = false;
|
||||
return;
|
||||
}
|
||||
isSwitching = true;
|
||||
@@ -124,10 +117,14 @@ Item {
|
||||
Connections {
|
||||
target: MprisController
|
||||
function onAvailablePlayersChanged() {
|
||||
if ((MprisController.availablePlayers?.length || 0) === 0)
|
||||
const count = (MprisController.availablePlayers?.length || 0);
|
||||
if (count === 0) {
|
||||
isSwitching = false;
|
||||
_switchHold = true;
|
||||
_switchHoldTimer.restart();
|
||||
_switchHold = false;
|
||||
} else {
|
||||
_switchHold = true;
|
||||
_switchHoldTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,95 +290,34 @@ Item {
|
||||
Item {
|
||||
id: bgContainer
|
||||
anchors.fill: parent
|
||||
visible: TrackArtService.resolvedArtUrl !== ""
|
||||
|
||||
// Fall back to the live mpris url so the background is never blank.
|
||||
readonly property string curArt: {
|
||||
const resolved = TrackArtService.resolvedArtUrl;
|
||||
if (resolved !== "")
|
||||
return resolved;
|
||||
const p = root.activePlayer;
|
||||
if (!p)
|
||||
return "";
|
||||
if (p.trackArtUrl)
|
||||
return p.trackArtUrl;
|
||||
const m = p.metadata;
|
||||
return m && m["mpris:artUrl"] ? m["mpris:artUrl"].toString() : "";
|
||||
}
|
||||
// Two layers crossfade: new art loads into the hidden one and fades in once decoded.
|
||||
property bool _showA: true
|
||||
visible: layerA.ready || layerB.ready
|
||||
|
||||
onCurArtChanged: syncArt()
|
||||
Component.onCompleted: syncArt()
|
||||
|
||||
function syncArt() {
|
||||
if (curArt === "")
|
||||
return;
|
||||
const front = _showA ? layerA : layerB;
|
||||
const back = _showA ? layerB : layerA;
|
||||
if (front.art == curArt)
|
||||
return;
|
||||
if (back.art == curArt) {
|
||||
// Already decoded in the hidden layer; flip once ready (else promote() does).
|
||||
if (back.ready)
|
||||
_showA = !_showA;
|
||||
return;
|
||||
Image {
|
||||
id: bgImage
|
||||
anchors.centerIn: parent
|
||||
width: Math.max(parent.width, parent.height) * 1.1
|
||||
height: width
|
||||
source: TrackArtService.resolvedArtUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: true
|
||||
visible: false
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready)
|
||||
maybeFinishSwitch();
|
||||
}
|
||||
back.art = curArt;
|
||||
}
|
||||
|
||||
// Flip to the hidden layer only when it holds the current art, ignoring stale
|
||||
// Ready re-emits (e.g. popout re-expose) that would otherwise ping-pong _showA.
|
||||
function promote(layer) {
|
||||
const back = _showA ? layerB : layerA;
|
||||
if (layer !== back)
|
||||
return;
|
||||
if (layer.art != curArt)
|
||||
return;
|
||||
_showA = (layer === layerA);
|
||||
root.maybeFinishSwitch();
|
||||
}
|
||||
|
||||
component BgBlurLayer: ClippingRectangle {
|
||||
id: layer
|
||||
property alias art: layerImg.source
|
||||
readonly property bool ready: layerImg.status === Image.Ready && layerImg.source != ""
|
||||
property bool front: false
|
||||
signal loaded
|
||||
|
||||
Item {
|
||||
id: blurredBg
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
antialiasing: true
|
||||
opacity: front ? 0.7 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: layerImg
|
||||
anchors.centerIn: parent
|
||||
width: Math.max(parent.width, parent.height) * 1.1
|
||||
height: width
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: true
|
||||
visible: false
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready && source != "")
|
||||
layer.loaded();
|
||||
}
|
||||
}
|
||||
visible: false
|
||||
|
||||
MultiEffect {
|
||||
anchors.centerIn: parent
|
||||
width: layerImg.width
|
||||
height: layerImg.height
|
||||
source: layerImg
|
||||
width: bgImage.width
|
||||
height: bgImage.height
|
||||
source: bgImage
|
||||
blurEnabled: true
|
||||
blurMax: 64
|
||||
blur: 0.8
|
||||
@@ -390,16 +326,22 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
BgBlurLayer {
|
||||
id: layerA
|
||||
front: bgContainer._showA
|
||||
onLoaded: bgContainer.promote(layerA)
|
||||
Rectangle {
|
||||
id: bgMask
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
BgBlurLayer {
|
||||
id: layerB
|
||||
front: !bgContainer._showA
|
||||
onLoaded: bgContainer.promote(layerB)
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: blurredBg
|
||||
maskEnabled: true
|
||||
maskSource: bgMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -500,8 +442,7 @@ Item {
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 1
|
||||
// Reserve the line so late album metadata doesn't shift the seekbar.
|
||||
height: Math.max(implicitHeight, Theme.fontSizeSmall * 1.4)
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,13 +531,13 @@ Item {
|
||||
height: 40
|
||||
radius: 20
|
||||
anchors.centerIn: parent
|
||||
color: shuffleArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
|
||||
color: shuffleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "shuffle"
|
||||
size: 20
|
||||
color: activePlayer && activePlayer.shuffle ? root.accent : Theme.surfaceText
|
||||
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -652,13 +593,13 @@ Item {
|
||||
height: 50
|
||||
radius: 25
|
||||
anchors.centerIn: parent
|
||||
color: root.accent
|
||||
color: Theme.primary
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
size: 28
|
||||
color: root.onAccent
|
||||
color: Theme.background
|
||||
weight: 500
|
||||
}
|
||||
|
||||
@@ -722,7 +663,7 @@ Item {
|
||||
height: 40
|
||||
radius: 20
|
||||
anchors.centerIn: parent
|
||||
color: repeatArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
|
||||
color: repeatArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
@@ -739,7 +680,7 @@ Item {
|
||||
}
|
||||
}
|
||||
size: 20
|
||||
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText
|
||||
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -778,7 +719,7 @@ Item {
|
||||
radius: 20
|
||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||
y: 185
|
||||
color: playerSelectorArea.containsMouse || playersExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||
color: playerSelectorArea.containsMouse || playersExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
z: 100
|
||||
@@ -845,7 +786,7 @@ Item {
|
||||
radius: 20
|
||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||
y: 130
|
||||
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
|
||||
border.width: 1
|
||||
z: 101
|
||||
@@ -857,7 +798,7 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
name: getVolumeIcon()
|
||||
size: 18
|
||||
color: volumeAvailable && currentVolume > 0 ? root.accent : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
|
||||
color: volumeAvailable && currentVolume > 0 ? Theme.primary : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -908,7 +849,7 @@ Item {
|
||||
radius: 20
|
||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||
y: 240
|
||||
color: audioDevicesArea.containsMouse || devicesExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||
color: audioDevicesArea.containsMouse || devicesExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
z: 100
|
||||
|
||||
@@ -86,10 +86,12 @@ Rectangle {
|
||||
}
|
||||
|
||||
function updateSelectedDateEvents() {
|
||||
const events = (CalendarService && CalendarService.calendarAvailable) ? CalendarService.getEventsForDate(selectedDate) : [];
|
||||
if (JSON.stringify(events) === JSON.stringify(selectedDateEvents))
|
||||
return;
|
||||
selectedDateEvents = events;
|
||||
if (CalendarService && CalendarService.calendarAvailable) {
|
||||
const events = CalendarService.getEventsForDate(selectedDate);
|
||||
selectedDateEvents = events;
|
||||
} else {
|
||||
selectedDateEvents = [];
|
||||
}
|
||||
}
|
||||
|
||||
function loadEventsForMonth() {
|
||||
@@ -274,29 +276,57 @@ Rectangle {
|
||||
height: 40
|
||||
visible: showEventDetails
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 32
|
||||
iconSize: 14
|
||||
iconName: "arrow_back"
|
||||
iconColor: Theme.primary
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
onClicked: root.showEventDetails = false
|
||||
radius: Theme.cornerRadius
|
||||
color: backButtonArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: 14
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.showEventDetails = false
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 32
|
||||
iconSize: 16
|
||||
iconName: "event"
|
||||
iconColor: Theme.primary
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
radius: Theme.cornerRadius
|
||||
visible: CalendarService && CalendarService.canCreateEvents
|
||||
onClicked: {
|
||||
root.editorEvent = null;
|
||||
root.showEditor = true;
|
||||
color: addEventArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "event"
|
||||
size: 16
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: addEventArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.editorEvent = null;
|
||||
root.showEditor = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,12 +358,31 @@ Rectangle {
|
||||
height: 28
|
||||
visible: !showEventDetails
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 28
|
||||
iconSize: 14
|
||||
iconName: "chevron_left"
|
||||
iconColor: Theme.primary
|
||||
onClicked: root.shiftMonth(-1)
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: prevMonthArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "chevron_left"
|
||||
size: 14
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevMonthArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let newDate = new Date(calendarGrid.displayDate);
|
||||
newDate.setMonth(newDate.getMonth() - 1);
|
||||
calendarGrid.displayDate = newDate;
|
||||
loadEventsForMonth();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -347,20 +396,53 @@ Rectangle {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 28
|
||||
iconSize: 14
|
||||
iconName: "today"
|
||||
iconColor: Theme.primary
|
||||
onClicked: root.goToToday()
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: todayArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "today"
|
||||
size: 14
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: todayArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.goToToday()
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 28
|
||||
iconSize: 14
|
||||
iconName: "chevron_right"
|
||||
iconColor: Theme.primary
|
||||
onClicked: root.shiftMonth(1)
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: nextMonthArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "chevron_right"
|
||||
size: 14
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextMonthArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let newDate = new Date(calendarGrid.displayDate);
|
||||
newDate.setMonth(newDate.getMonth() + 1);
|
||||
calendarGrid.displayDate = newDate;
|
||||
loadEventsForMonth();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,16 +814,12 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool isLocalTask: taskId.startsWith("task_")
|
||||
readonly property bool isDankTask: taskId.startsWith("vtodo_")
|
||||
readonly property bool isTask: isLocalTask || isDankTask
|
||||
readonly property bool canModify: isLocalTask || (isDankTask && modelData && !modelData.readOnly)
|
||||
readonly property color baseAccent: {
|
||||
if (isLocalTask || !modelData || !modelData.color || !modelData.color.length)
|
||||
return Theme.primary;
|
||||
return modelData.color;
|
||||
readonly property bool isTask: modelData && modelData.id && modelData.id.startsWith("task_")
|
||||
readonly property color accentColor: {
|
||||
if (isTask)
|
||||
return modelData.completed ? Theme.withAlpha(Theme.primary, 0.4) : Theme.primary;
|
||||
return (modelData && modelData.color && modelData.color.length) ? modelData.color : Theme.primary;
|
||||
}
|
||||
readonly property color accentColor: (isTask && modelData && modelData.completed) ? Theme.withAlpha(baseAccent, 0.4) : baseAccent
|
||||
readonly property color surfaceColor: isDragging ? Theme.primaryPressed : (eventMouseArea.containsMouse ? Theme.primaryBackground : Theme.nestedSurface)
|
||||
|
||||
color: surfaceColor
|
||||
@@ -810,13 +888,13 @@ Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
visible: taskItem.isLocalTask && !taskItem.isEditing
|
||||
visible: modelData && modelData.id && modelData.id.startsWith("task_") && !taskItem.isEditing
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "drag_indicator"
|
||||
size: 14
|
||||
color: dragMouseArea.containsMouse ? Theme.primary : Theme.surfaceTextMedium
|
||||
color: dragMouseArea.containsMouse ? Theme.primary : Theme.surfaceTextAlpha
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -859,14 +937,34 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Checkbox status icon
|
||||
Rectangle {
|
||||
id: checkboxContainer
|
||||
width: 24
|
||||
height: 24
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? (taskItem.isEditing ? 8 : 32) : 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
visible: modelData && modelData.id && modelData.id.startsWith("task_")
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: (modelData && modelData.completed) ? "check_box" : "check_box_outline_blank"
|
||||
size: 16
|
||||
color: (modelData && modelData.completed) ? Theme.primary : Theme.onSurface_38
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: eventContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: taskItem.isLocalTask ? 60 : taskItem.isDankTask ? 36 : (Theme.spacingS + 6)
|
||||
anchors.rightMargin: taskItem.canModify ? 64 : Theme.spacingXS
|
||||
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 60 : (Theme.spacingS + 6)
|
||||
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : Theme.spacingXS
|
||||
spacing: Theme.spacingXXS
|
||||
visible: !taskItem.isEditing
|
||||
|
||||
@@ -874,7 +972,7 @@ Rectangle {
|
||||
width: parent.width
|
||||
text: modelData ? modelData.title : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: (taskItem.isTask && modelData && modelData.completed) ? Theme.surfaceTextSecondary : Theme.surfaceText
|
||||
color: (modelData && modelData.id && modelData.id.startsWith("task_") && modelData.completed) ? Theme.surfaceTextSecondary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
elide: Text.ElideRight
|
||||
@@ -902,7 +1000,7 @@ Rectangle {
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Normal
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
visible: text !== "" && !taskItem.isLocalTask
|
||||
visible: text !== "" && modelData && modelData.id && !modelData.id.startsWith("task_")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,73 +1047,90 @@ Rectangle {
|
||||
id: eventMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: taskItem.isLocalTask ? 32 : 6
|
||||
anchors.rightMargin: taskItem.canModify ? 64 : 0
|
||||
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 32 : 6
|
||||
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : 0
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: modelData && !taskItem.isEditing
|
||||
onClicked: {
|
||||
if (!modelData)
|
||||
return;
|
||||
if (taskItem.isTask && taskItem.canModify) {
|
||||
if (modelData && modelData.id && modelData.id.startsWith("task_")) {
|
||||
CalendarService.toggleTask(modelData.id);
|
||||
return;
|
||||
}
|
||||
root.detailEvent = modelData;
|
||||
if (modelData)
|
||||
root.detailEvent = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 24
|
||||
iconSize: 16
|
||||
iconName: (modelData && modelData.completed) ? "check_box" : "check_box_outline_blank"
|
||||
iconColor: (modelData && modelData.completed) ? Theme.primary : Theme.surfaceText
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: taskItem.isLocalTask ? (taskItem.isEditing ? 8 : 32) : 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: taskItem.isTask
|
||||
enabled: taskItem.canModify && !taskItem.isEditing
|
||||
onClicked: CalendarService.toggleTask(modelData.id)
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
// Delete / Cancel Button
|
||||
Rectangle {
|
||||
id: deleteButton
|
||||
buttonSize: 24
|
||||
iconSize: 14
|
||||
iconName: taskItem.isEditing ? "close" : "delete"
|
||||
iconColor: taskItem.isEditing ? Theme.surfaceText : Theme.error
|
||||
width: 24
|
||||
height: 24
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: taskItem.canModify
|
||||
onClicked: {
|
||||
if (taskItem.isEditing) {
|
||||
taskItem.isEditing = false;
|
||||
return;
|
||||
radius: Theme.cornerRadius
|
||||
color: deleteMouseArea.containsMouse ? (taskItem.isEditing ? Theme.primaryHover : Qt.rgba(0.9, 0.2, 0.2, 0.15)) : Theme.withAlpha(Qt.rgba(0.9, 0.2, 0.2, 0.15), 0)
|
||||
visible: modelData && modelData.id && modelData.id.startsWith("task_")
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: taskItem.isEditing ? "close" : "delete"
|
||||
size: 14
|
||||
color: deleteMouseArea.containsMouse ? (taskItem.isEditing ? Theme.primary : Qt.rgba(0.9, 0.2, 0.2, 1.0)) : Theme.onSurface_38
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deleteMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (taskItem.isEditing) {
|
||||
taskItem.isEditing = false;
|
||||
} else if (modelData && modelData.id) {
|
||||
CalendarService.removeTask(modelData.id);
|
||||
}
|
||||
}
|
||||
if (modelData && modelData.id)
|
||||
CalendarService.removeTask(modelData.id);
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 24
|
||||
iconSize: 14
|
||||
iconName: taskItem.isEditing ? "check" : "edit"
|
||||
iconColor: taskItem.isEditing ? Theme.primary : Theme.surfaceText
|
||||
// Edit / Save Button
|
||||
Rectangle {
|
||||
id: editButton
|
||||
width: 24
|
||||
height: 24
|
||||
anchors.right: deleteButton.left
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: taskItem.canModify
|
||||
onClicked: {
|
||||
if (!taskItem.isEditing) {
|
||||
taskItem.isEditing = true;
|
||||
return;
|
||||
radius: Theme.cornerRadius
|
||||
color: editMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
visible: modelData && modelData.id && modelData.id.startsWith("task_")
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: taskItem.isEditing ? "check" : "edit"
|
||||
size: 14
|
||||
color: editMouseArea.containsMouse ? Theme.primary : Theme.onSurface_38
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: editMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (taskItem.isEditing) {
|
||||
let txt = editInput.text.trim();
|
||||
if (txt !== "" && modelData && modelData.id) {
|
||||
CalendarService.editTask(modelData.id, txt);
|
||||
}
|
||||
taskItem.isEditing = false;
|
||||
} else {
|
||||
taskItem.isEditing = true;
|
||||
}
|
||||
}
|
||||
let txt = editInput.text.trim();
|
||||
if (txt !== "" && modelData && modelData.id)
|
||||
CalendarService.editTask(modelData.id, txt);
|
||||
taskItem.isEditing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,13 +154,13 @@ Card {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: MediaAccentService.accent
|
||||
color: Theme.primary
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
size: 16
|
||||
color: MediaAccentService.onAccent
|
||||
color: Theme.background
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.DankDash.Overview
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -18,7 +17,7 @@ Item {
|
||||
signal navFocusRequested
|
||||
|
||||
function handleKeyEvent(event) {
|
||||
return calendarLoader.item ? calendarLoader.item.handleKeyEvent(event) : false;
|
||||
return calendarCard.handleKeyEvent(event);
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -58,26 +57,16 @@ Item {
|
||||
height: 220
|
||||
}
|
||||
|
||||
// Calendar - bottom middle; deferred so the grid stays off the emerge frame.
|
||||
Loader {
|
||||
id: calendarLoader
|
||||
// Calendar - bottom middle (wider and taller)
|
||||
CalendarOverviewCard {
|
||||
id: calendarCard
|
||||
x: parent.width * 0.2 - Theme.spacingM
|
||||
y: 100 + Theme.spacingM
|
||||
width: parent.width * 0.6
|
||||
height: 300
|
||||
asynchronous: true
|
||||
sourceComponent: Component {
|
||||
CalendarOverviewCard {
|
||||
onCloseDash: root.closeDash()
|
||||
onNavFocusRequested: root.navFocusRequested()
|
||||
}
|
||||
}
|
||||
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
size: 32
|
||||
visible: calendarLoader.status === Loader.Loading
|
||||
}
|
||||
onCloseDash: root.closeDash()
|
||||
onNavFocusRequested: root.navFocusRequested()
|
||||
}
|
||||
|
||||
// Media - bottom right (narrow and taller)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Qt.labs.folderlistmodel
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Widgets
|
||||
@@ -21,11 +20,12 @@ Item {
|
||||
property int itemsPerPage: 16
|
||||
property int totalPages: Math.max(1, Math.ceil(wallpaperFolderModel.count / itemsPerPage))
|
||||
property bool active: false
|
||||
property Item focusTarget: pager
|
||||
property Item focusTarget: wallpaperGrid
|
||||
property Item tabBarItem: null
|
||||
property int gridIndex: 0
|
||||
property Item keyForwardTarget: null
|
||||
property var parentPopout: null
|
||||
property int lastPage: 0
|
||||
property bool enableAnimation: false
|
||||
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
property string selectedFileName: ""
|
||||
@@ -86,12 +86,12 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentPageChanged: updateSelectedFileName()
|
||||
|
||||
onTotalPagesChanged: {
|
||||
if (currentPage >= totalPages) {
|
||||
currentPage = Math.max(0, totalPages - 1);
|
||||
onCurrentPageChanged: {
|
||||
if (currentPage !== lastPage) {
|
||||
enableAnimation = false;
|
||||
lastPage = currentPage;
|
||||
}
|
||||
updateSelectedFileName();
|
||||
}
|
||||
|
||||
onGridIndexChanged: {
|
||||
@@ -257,7 +257,6 @@ Item {
|
||||
}
|
||||
|
||||
function setInitialSelection() {
|
||||
enableAnimation = false;
|
||||
const currentWallpaper = getCurrentWallpaper();
|
||||
if (!currentWallpaper || wallpaperFolderModel.count === 0) {
|
||||
gridIndex = 0;
|
||||
@@ -351,6 +350,17 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function collectWallpaperPaths() {
|
||||
const paths = [];
|
||||
for (var i = 0; i < wallpaperFolderModel.count; i++) {
|
||||
const filePath = wallpaperFolderModel.get(i, "filePath");
|
||||
if (filePath) {
|
||||
paths.push(filePath.toString().replace(/^file:\/\//, ''));
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: wallpaperFolderModel
|
||||
function onCountChanged() {
|
||||
@@ -359,6 +369,7 @@ Item {
|
||||
setInitialSelection();
|
||||
}
|
||||
updateSelectedFileName();
|
||||
thumbnailPreloader.paths = collectWallpaperPaths();
|
||||
}
|
||||
}
|
||||
function onStatusChanged() {
|
||||
@@ -367,10 +378,16 @@ Item {
|
||||
setInitialSelection();
|
||||
}
|
||||
updateSelectedFileName();
|
||||
thumbnailPreloader.paths = collectWallpaperPaths();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WallpaperThumbnailPreloader {
|
||||
id: thumbnailPreloader
|
||||
cacheSize: 256
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: wallpaperFolderModel
|
||||
|
||||
@@ -430,169 +447,147 @@ Item {
|
||||
width: parent.width
|
||||
height: parent.height - 50
|
||||
|
||||
ListView {
|
||||
id: pager
|
||||
GridView {
|
||||
id: wallpaperGrid
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingS
|
||||
height: parent.height - Theme.spacingS
|
||||
orientation: ListView.Vertical
|
||||
snapMode: ListView.SnapOneItem
|
||||
highlightRangeMode: ListView.StrictlyEnforceRange
|
||||
preferredHighlightBegin: 0
|
||||
preferredHighlightEnd: height
|
||||
highlightMoveDuration: root.enableAnimation ? Theme.mediumDuration : 0
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cellWidth: width / 4
|
||||
cellHeight: height / 4
|
||||
clip: true
|
||||
enabled: root.active
|
||||
interactive: root.active
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
keyNavigationEnabled: false
|
||||
activeFocusOnTab: false
|
||||
highlightFollowsCurrentItem: true
|
||||
highlightMoveDuration: enableAnimation ? Theme.shortDuration : 0
|
||||
focus: false
|
||||
cacheBuffer: Math.max(0, height * 2)
|
||||
reuseItems: true
|
||||
model: root.totalPages
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (!moving) {
|
||||
return;
|
||||
}
|
||||
if (currentIndex >= 0 && currentIndex !== root.currentPage) {
|
||||
root.currentPage = currentIndex;
|
||||
highlight: Item {
|
||||
z: 1000
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
color: "transparent"
|
||||
border.width: 3
|
||||
border.color: Theme.primary
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: currentIndex = root.currentPage
|
||||
model: {
|
||||
root.gridRevision; // re-evaluate when sort order changes in place
|
||||
const startIndex = currentPage * itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count);
|
||||
const items = [];
|
||||
for (var i = startIndex; i < endIndex; i++) {
|
||||
const filePath = wallpaperFolderModel.get(i, "filePath");
|
||||
if (filePath) {
|
||||
items.push(filePath.toString().replace(/^file:\/\//, ''));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
onModelChanged: {
|
||||
const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0;
|
||||
if (gridIndex !== clampedIndex) {
|
||||
gridIndex = clampedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
if (count > 0) {
|
||||
const clampedIndex = Math.min(gridIndex, count - 1);
|
||||
currentIndex = clampedIndex;
|
||||
positionViewAtIndex(clampedIndex, GridView.Contain);
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
enableAnimation = true;
|
||||
});
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCurrentPageChanged() {
|
||||
if (pager.currentIndex !== root.currentPage) {
|
||||
pager.currentIndex = root.currentPage;
|
||||
function onGridIndexChanged() {
|
||||
if (wallpaperGrid.count > 0) {
|
||||
wallpaperGrid.currentIndex = gridIndex;
|
||||
if (!enableAnimation) {
|
||||
wallpaperGrid.positionViewAtIndex(gridIndex, GridView.Contain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: GridView {
|
||||
id: pageGrid
|
||||
delegate: Item {
|
||||
width: wallpaperGrid.cellWidth
|
||||
height: wallpaperGrid.cellHeight
|
||||
|
||||
property int pageIndex: index
|
||||
property string wallpaperPath: modelData || ""
|
||||
property bool isSelected: getCurrentWallpaper() === modelData
|
||||
|
||||
width: pager.width
|
||||
height: pager.height
|
||||
cellWidth: width / 4
|
||||
cellHeight: height / 4
|
||||
interactive: false
|
||||
keyNavigationEnabled: false
|
||||
activeFocusOnTab: false
|
||||
focus: false
|
||||
highlightFollowsCurrentItem: true
|
||||
highlightMoveDuration: root.enableAnimation ? Theme.shortDuration : 0
|
||||
currentIndex: root.currentPage === pageIndex ? root.gridIndex : -1
|
||||
|
||||
highlight: Item {
|
||||
z: 1000
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
color: "transparent"
|
||||
border.width: 3
|
||||
border.color: Theme.primary
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
reuseItems: true
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
root.gridRevision; // re-evaluate when sort order changes in place
|
||||
const startIndex = pageGrid.pageIndex * root.itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + root.itemsPerPage, wallpaperFolderModel.count);
|
||||
const items = [];
|
||||
for (var i = startIndex; i < endIndex; i++) {
|
||||
const filePath = wallpaperFolderModel.get(i, "filePath");
|
||||
if (filePath) {
|
||||
items.push(filePath.toString().replace(/^file:\/\//, ''));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
if (root.currentPage !== pageIndex || count === 0) {
|
||||
return;
|
||||
}
|
||||
if (root.gridIndex >= count) {
|
||||
root.gridIndex = count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
width: pageGrid.cellWidth
|
||||
height: pageGrid.cellHeight
|
||||
|
||||
property string wallpaperPath: modelData || ""
|
||||
property bool isSelected: getCurrentWallpaper() === modelData
|
||||
Rectangle {
|
||||
id: wallpaperCard
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: wallpaperCard
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
radius: parent.radius
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: maskRect
|
||||
width: thumbnailImage.width
|
||||
height: thumbnailImage.height
|
||||
radius: Theme.cornerRadius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
radius: parent.radius
|
||||
CachingImage {
|
||||
id: thumbnailImage
|
||||
anchors.fill: parent
|
||||
imagePath: modelData || ""
|
||||
maxCacheSize: 256
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
maskSource: maskRect
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: wallpaperCard.radius
|
||||
color: "transparent"
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
cornerRadius: parent.radius
|
||||
stateColor: Theme.primary
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
id: thumbnailImage
|
||||
anchors.fill: parent
|
||||
imagePath: modelData || ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
MouseArea {
|
||||
id: wallpaperMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
cornerRadius: parent.radius
|
||||
stateColor: Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wallpaperMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
gridIndex = index;
|
||||
if (modelData) {
|
||||
setCurrentWallpaper(modelData);
|
||||
}
|
||||
onClicked: {
|
||||
gridIndex = index;
|
||||
if (modelData) {
|
||||
setCurrentWallpaper(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -600,15 +595,9 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
size: 40
|
||||
visible: wallpaperFolderModel.status === FolderListModel.Loading && wallpaperFolderModel.count === 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
visible: wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count === 0
|
||||
visible: wallpaperFolderModel.count === 0
|
||||
text: I18n.tr("No wallpapers found\n\nClick the folder icon below to browse")
|
||||
font.pixelSize: 14
|
||||
color: Theme.outline
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -244,6 +245,17 @@ Item {
|
||||
size: Theme.iconSize * 2
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
layer.enabled: Theme.elevationEnabled
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1)
|
||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1)
|
||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0
|
||||
blurMax: Theme.elevationBlurMax
|
||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1)
|
||||
shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -550,287 +562,295 @@ Item {
|
||||
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
opacity: skyBox.backgroundOpacity
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Theme.withAlpha(skyBox.blackColor, 0.0)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.05
|
||||
color: skyBox.topColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: skyBox.topColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: skyBox.topColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.501
|
||||
color: skyBox.blackColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.9
|
||||
color: skyBox.blackColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Theme.withAlpha(skyBox.blackColor, 0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var currentDate: dateStepper.currentDate
|
||||
property var hMargin: 0
|
||||
property var vMargin: Theme.spacingM
|
||||
property var effectiveHeight: skyBox.height - 2 * vMargin
|
||||
property var effectiveWidth: skyBox.width - 2 * hMargin
|
||||
|
||||
StyledText {
|
||||
text: parent.sunTime?.period ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: skyShape
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
height: parent.height / 2
|
||||
opacity: skyBox.backgroundOpacity
|
||||
|
||||
ShapePath {
|
||||
strokeColor: "transparent"
|
||||
fillGradient: RadialGradient {
|
||||
centerX: skyBox.hMargin + sun.x + sun.width / 2
|
||||
centerY: skyBox.vMargin + sun.y + 30
|
||||
centerRadius: {
|
||||
const a = Math.abs((skyBox.sunTime?.dayPercent ?? 0) - 0.5);
|
||||
const out = 200 * (0.5 - a * a);
|
||||
return out;
|
||||
}
|
||||
focalX: skyBox.hMargin + sun.x + sun.width / 2
|
||||
focalY: skyBox.vMargin + sun.y
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: skyBox.sunColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: Theme.blendAlpha(skyBox.sunColor, 0.5)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: skyShape.width
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: skyShape.width
|
||||
y: skyShape.height
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: skyShape.height
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
strokeColor: "transparent"
|
||||
fillGradient: RadialGradient {
|
||||
centerX: sun.x
|
||||
centerY: sun.y
|
||||
centerRadius: 500
|
||||
focalX: centerX
|
||||
focalY: centerY + 0.99 * (centerRadius - focalRadius)
|
||||
focalRadius: 10
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: skyBox.sunColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.45
|
||||
color: skyBox.sunColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.55
|
||||
color: "transparent"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: skyShape.width
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: skyShape.width
|
||||
y: skyShape.height
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: skyShape.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: ecliptic
|
||||
anchors.fill: parent
|
||||
property var points: WeatherService.getEcliptic(dateStepper.currentDate)
|
||||
|
||||
function getX(index) {
|
||||
return points[index].h * skyBox.effectiveWidth + skyBox.hMargin;
|
||||
}
|
||||
function getY(index) {
|
||||
return points[index].v * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 + skyBox.vMargin;
|
||||
}
|
||||
|
||||
onPointsChanged: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
if (!points || points.length === 0)
|
||||
return;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(getX(0), getY(0));
|
||||
for (var i = 1; i < points.length; i++) {
|
||||
ctx.lineTo(getX(i), getY(i));
|
||||
}
|
||||
ctx.strokeStyle = Theme.withAlpha(Theme.outline, 0.2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
property real latitude: WeatherService.getLocation()?.latitude ?? 0
|
||||
property real sunDeclination: WeatherService.getSunDeclination(dateStepper.currentDate)
|
||||
|
||||
readonly property bool solarNoonIsSouth: latitude > sunDeclination
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
sourceComponent: skyContentComponent
|
||||
opacity: status === Loader.Ready ? 1 : 0
|
||||
StyledText {
|
||||
id: middle
|
||||
text: skyBox.solarNoonIsSouth ? "S" : "N"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
x: skyBox.width / 2 - middle.width / 2
|
||||
y: skyBox.height / 2 - middle.height / 2
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
StyledText {
|
||||
id: left
|
||||
text: skyBox.solarNoonIsSouth ? "E" : "W"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
x: skyBox.width / 4 - left.width / 2
|
||||
y: skyBox.height / 2 - left.height / 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: right
|
||||
text: skyBox.solarNoonIsSouth ? "W" : "E"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
x: 3 * skyBox.width / 4 - right.width / 2
|
||||
y: skyBox.height / 2 - right.height / 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.left: right.right
|
||||
anchors.right: skyBox.right
|
||||
anchors.verticalCenter: middle.verticalCenter
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.left: middle.right
|
||||
anchors.right: right.left
|
||||
anchors.verticalCenter: middle.verticalCenter
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.left: left.right
|
||||
anchors.right: middle.left
|
||||
anchors.verticalCenter: middle.verticalCenter
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.left: skyBox.left
|
||||
anchors.right: left.left
|
||||
anchors.verticalCenter: middle.verticalCenter
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
id: moonPhase
|
||||
name: WeatherService.getMoonPhase(skyBox.currentDate) || ""
|
||||
size: Theme.fontSizeXLarge
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||
rotation: (WeatherService.getMoonAngle(skyBox.currentDate) || 0) / Math.PI * 180
|
||||
visible: !!pos
|
||||
|
||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
|
||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
|
||||
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
|
||||
|
||||
layer.enabled: Theme.elevationEnabled
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
|
||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
|
||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
|
||||
blurMax: Theme.elevationBlurMax
|
||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: skyContentComponent
|
||||
DankIcon {
|
||||
id: sun
|
||||
name: "light_mode"
|
||||
size: Theme.fontSizeXLarge
|
||||
color: Theme.primary
|
||||
visible: !!pos
|
||||
|
||||
Item {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
opacity: skyBox.backgroundOpacity
|
||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
|
||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
|
||||
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Theme.withAlpha(skyBox.blackColor, 0.0)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.05
|
||||
color: skyBox.topColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: skyBox.topColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: skyBox.topColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.501
|
||||
color: skyBox.blackColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.9
|
||||
color: skyBox.blackColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Theme.withAlpha(skyBox.blackColor, 0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: skyBox.sunTime?.period ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: skyShape
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
height: parent.height / 2
|
||||
opacity: skyBox.backgroundOpacity
|
||||
|
||||
ShapePath {
|
||||
strokeColor: "transparent"
|
||||
fillGradient: RadialGradient {
|
||||
centerX: skyBox.hMargin + sun.x + sun.width / 2
|
||||
centerY: skyBox.vMargin + sun.y + 30
|
||||
centerRadius: {
|
||||
const a = Math.abs((skyBox.sunTime?.dayPercent ?? 0) - 0.5);
|
||||
const out = 200 * (0.5 - a * a);
|
||||
return out;
|
||||
}
|
||||
focalX: skyBox.hMargin + sun.x + sun.width / 2
|
||||
focalY: skyBox.vMargin + sun.y
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: skyBox.sunColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: Theme.blendAlpha(skyBox.sunColor, 0.5)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: skyShape.width
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: skyShape.width
|
||||
y: skyShape.height
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: skyShape.height
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
strokeColor: "transparent"
|
||||
fillGradient: RadialGradient {
|
||||
centerX: sun.x
|
||||
centerY: sun.y
|
||||
centerRadius: 500
|
||||
focalX: centerX
|
||||
focalY: centerY + 0.99 * (centerRadius - focalRadius)
|
||||
focalRadius: 10
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: skyBox.sunColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.45
|
||||
color: skyBox.sunColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.55
|
||||
color: "transparent"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: skyShape.width
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: skyShape.width
|
||||
y: skyShape.height
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: skyShape.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
|
||||
ShapePath {
|
||||
strokeColor: Theme.withAlpha(Theme.outline, 0.2)
|
||||
strokeWidth: 1
|
||||
fillColor: "transparent"
|
||||
|
||||
PathPolyline {
|
||||
path: {
|
||||
const points = WeatherService.getEcliptic(dateStepper.currentDate) ?? [];
|
||||
const out = [];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
out.push(Qt.point(points[i].h * skyBox.effectiveWidth + skyBox.hMargin, points[i].v * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 + skyBox.vMargin));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: middle
|
||||
text: skyBox.solarNoonIsSouth ? "S" : "N"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
x: skyBox.width / 2 - middle.width / 2
|
||||
y: skyBox.height / 2 - middle.height / 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: left
|
||||
text: skyBox.solarNoonIsSouth ? "E" : "W"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
x: skyBox.width / 4 - left.width / 2
|
||||
y: skyBox.height / 2 - left.height / 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: right
|
||||
text: skyBox.solarNoonIsSouth ? "W" : "E"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
x: 3 * skyBox.width / 4 - right.width / 2
|
||||
y: skyBox.height / 2 - right.height / 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.left: right.right
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: middle.verticalCenter
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.left: middle.right
|
||||
anchors.right: right.left
|
||||
anchors.verticalCenter: middle.verticalCenter
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.left: left.right
|
||||
anchors.right: middle.left
|
||||
anchors.verticalCenter: middle.verticalCenter
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.left: parent.left
|
||||
anchors.right: left.left
|
||||
anchors.verticalCenter: middle.verticalCenter
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
id: moonPhase
|
||||
name: WeatherService.getMoonPhase(skyBox.currentDate) || ""
|
||||
size: Theme.fontSizeXLarge
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||
rotation: (WeatherService.getMoonAngle(skyBox.currentDate) || 0) / Math.PI * 180
|
||||
visible: !!pos
|
||||
|
||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
|
||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
|
||||
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: sun
|
||||
name: "light_mode"
|
||||
size: Theme.fontSizeXLarge
|
||||
color: Theme.primary
|
||||
visible: !!pos
|
||||
|
||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
|
||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
|
||||
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
|
||||
layer.enabled: Theme.elevationEnabled
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
|
||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
|
||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
|
||||
blurMax: Theme.elevationBlurMax
|
||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -973,7 +993,6 @@ Item {
|
||||
ListView {
|
||||
id: hourlyList
|
||||
anchors.fill: parent
|
||||
reuseItems: true
|
||||
orientation: ListView.Horizontal
|
||||
spacing: Theme.spacingS
|
||||
clip: true
|
||||
@@ -1060,7 +1079,6 @@ Item {
|
||||
ListView {
|
||||
id: dailyList
|
||||
anchors.fill: parent
|
||||
reuseItems: true
|
||||
orientation: ListView.Horizontal
|
||||
spacing: Theme.spacingS
|
||||
clip: true
|
||||
|
||||
@@ -807,23 +807,22 @@ PanelWindow {
|
||||
|
||||
function _notifBodyScene() {
|
||||
const isHoriz = SurfaceGeometry.isHorizontal(win._notifDescriptor.barSide);
|
||||
const body = win._notifBodyGeometry;
|
||||
const start = win._notifStartUnderlapValue;
|
||||
const end = win._notifEndUnderlapValue;
|
||||
const side = win._notifSideUnderlapValue;
|
||||
if (isHoriz) {
|
||||
return {
|
||||
"x": body.x - start,
|
||||
"y": body.y,
|
||||
"width": body.width + start + end,
|
||||
"height": body.height
|
||||
"x": _notifBodyBlurAnchor.x - start,
|
||||
"y": _notifBodyBlurAnchor.y,
|
||||
"width": _notifBodyBlurAnchor.width + start + end,
|
||||
"height": _notifBodyBlurAnchor.height
|
||||
};
|
||||
}
|
||||
return {
|
||||
"x": body.x - (win._notifDescriptor.barSide === "left" ? side : 0),
|
||||
"y": body.y - start,
|
||||
"width": body.width + side,
|
||||
"height": body.height + start + end
|
||||
"x": _notifBodyBlurAnchor.x - (win._notifDescriptor.barSide === "left" ? side : 0),
|
||||
"y": _notifBodyBlurAnchor.y - start,
|
||||
"width": _notifBodyBlurAnchor.width + side,
|
||||
"height": _notifBodyBlurAnchor.height + start + end
|
||||
};
|
||||
}
|
||||
|
||||
@@ -867,9 +866,8 @@ PanelWindow {
|
||||
"surfaceRadius": win._surfaceRadius
|
||||
}
|
||||
});
|
||||
const n = win._notifBodyScene();
|
||||
const nb = win._notifBodyGeometry;
|
||||
if (win._frameActive && win._notifDescriptor.visible && nb.width > 0 && nb.height > 0)
|
||||
const n = win._notifBodyGeometry;
|
||||
if (win._frameActive && win._notifDescriptor.visible && n.width > 0 && n.height > 0)
|
||||
arr.push({
|
||||
"side": win._notifDescriptor.barSide,
|
||||
"body": {"x": n.x, "y": n.y, "width": n.width, "height": n.height},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -567,11 +567,24 @@ Rectangle {
|
||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
ClippingRectangle {
|
||||
Rectangle {
|
||||
id: gridProgressMask
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: gridButtonRect.isHolding
|
||||
layer.enabled: gridButtonRect.isHolding
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: gridProgressMask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
@@ -687,11 +700,24 @@ Rectangle {
|
||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
ClippingRectangle {
|
||||
Rectangle {
|
||||
id: listProgressMask
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: listButtonRect.isHolding
|
||||
layer.enabled: listButtonRect.isHolding
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: listProgressMask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -847,34 +847,6 @@ Item {
|
||||
cursorPosition = pos;
|
||||
}
|
||||
|
||||
function deleteToLineStart() {
|
||||
clampCursorPosition();
|
||||
if (cursorPosition === 0)
|
||||
return;
|
||||
root.passwordEdited(text.slice(cursorPosition));
|
||||
cursorPosition = 0;
|
||||
}
|
||||
|
||||
function deleteToLineEnd() {
|
||||
clampCursorPosition();
|
||||
if (cursorPosition === text.length)
|
||||
return;
|
||||
root.passwordEdited(text.slice(0, cursorPosition));
|
||||
}
|
||||
|
||||
function deleteWordBackward() {
|
||||
clampCursorPosition();
|
||||
if (cursorPosition === 0)
|
||||
return;
|
||||
let pos = cursorPosition;
|
||||
while (pos > 0 && text.charAt(pos - 1) === " ")
|
||||
pos--;
|
||||
while (pos > 0 && text.charAt(pos - 1) !== " ")
|
||||
pos--;
|
||||
root.passwordEdited(text.slice(0, pos) + text.slice(cursorPosition));
|
||||
cursorPosition = pos;
|
||||
}
|
||||
|
||||
function isPrintableText(value) {
|
||||
if (value.length === 0)
|
||||
return false;
|
||||
@@ -940,49 +912,6 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.modifiers & Qt.ControlModifier) && !(event.modifiers & (Qt.AltModifier | Qt.MetaModifier))) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_A:
|
||||
cursorPosition = 0;
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_E:
|
||||
cursorPosition = text.length;
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_B:
|
||||
clampCursorPosition();
|
||||
cursorPosition = Math.max(0, cursorPosition - 1);
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_F:
|
||||
clampCursorPosition();
|
||||
cursorPosition = Math.min(text.length, cursorPosition + 1);
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_U:
|
||||
deleteToLineStart();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_K:
|
||||
deleteToLineEnd();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_W:
|
||||
deleteWordBackward();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_H:
|
||||
backspace();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_D:
|
||||
deleteForward();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
|
||||
@@ -13,19 +13,12 @@ Column {
|
||||
property int draggedIndex: -1
|
||||
property int dropTargetIndex: -1
|
||||
property bool suppressShiftAnimation: false
|
||||
property int editingIndex: -1
|
||||
readonly property real tabItemSize: tabRow.dynamicTabWidth + Theme.spacingXS
|
||||
readonly property real tabItemSize: 128 + Theme.spacingXS
|
||||
|
||||
signal tabSwitched(int tabIndex)
|
||||
signal tabClosed(int tabIndex)
|
||||
signal newTabRequested
|
||||
|
||||
function commitRename(index, newTitle) {
|
||||
if (index >= 0)
|
||||
NotepadStorageService.renameTab(index, newTitle);
|
||||
editingIndex = -1;
|
||||
}
|
||||
|
||||
function hasUnsavedChangesForTab(tab) {
|
||||
if (!tab)
|
||||
return false;
|
||||
@@ -39,19 +32,11 @@ Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
id: tabRow
|
||||
width: parent.width
|
||||
height: 36
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
readonly property real dynamicTabWidth: {
|
||||
var count = Math.max(1, NotepadStorageService.tabs.length);
|
||||
var raw = (tabScroll.width - (count - 1) * Theme.spacingXS) / count;
|
||||
return Math.max(128, Math.min(300, raw));
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: tabScroll
|
||||
width: parent.width - newTabButton.width - Theme.spacingXS
|
||||
height: parent.height
|
||||
clip: true
|
||||
@@ -72,8 +57,7 @@ Column {
|
||||
|
||||
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
|
||||
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
|
||||
readonly property bool editing: root.editingIndex === index
|
||||
readonly property real tabWidth: tabRow.dynamicTabWidth
|
||||
readonly property real tabWidth: 128
|
||||
property bool longPressing: false
|
||||
property bool dragging: false
|
||||
property point dragStartPos: Qt.point(0, 0)
|
||||
@@ -154,7 +138,6 @@ Column {
|
||||
|
||||
StyledText {
|
||||
id: tabText
|
||||
visible: !delegateItem.editing
|
||||
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
|
||||
text: {
|
||||
var prefix = "";
|
||||
@@ -172,54 +155,13 @@ Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: renameField
|
||||
visible: delegateItem.editing
|
||||
enabled: delegateItem.editing
|
||||
width: parent.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primary
|
||||
selectionColor: Theme.primary
|
||||
selectedTextColor: Theme.background
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
|
||||
onEditingFinished: root.commitRename(index, text)
|
||||
Keys.onEscapePressed: event => {
|
||||
text = modelData.title || "Untitled";
|
||||
root.editingIndex = -1;
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
// A tab switch re-focuses the editor via Qt.callLater; the
|
||||
// timer fires afterwards so the field keeps focus + selection.
|
||||
Timer {
|
||||
id: renameFocusTimer
|
||||
interval: 20
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
renameField.forceActiveFocus();
|
||||
renameField.selectAll();
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
return;
|
||||
text = modelData.title || "Untitled";
|
||||
renameFocusTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: tabCloseButton
|
||||
width: 20
|
||||
height: 20
|
||||
radius: Theme.cornerRadius
|
||||
color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0)
|
||||
visible: NotepadStorageService.tabs.length > 1 && !delegateItem.editing
|
||||
visible: NotepadStorageService.tabs.length > 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
@@ -253,24 +195,11 @@ Column {
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: !delegateItem.editing
|
||||
hoverEnabled: true
|
||||
preventStealing: dragging || longPressing
|
||||
cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
onDoubleClicked: {
|
||||
root.tabSwitched(index);
|
||||
root.editingIndex = index;
|
||||
}
|
||||
|
||||
onExited: tabTooltip.hide()
|
||||
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse && tabText.truncated)
|
||||
tabTooltip.show(modelData.title || "Untitled", delegateItem, 0, 0, "bottom");
|
||||
}
|
||||
|
||||
onPressed: mouse => {
|
||||
if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) {
|
||||
delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||
@@ -350,8 +279,4 @@ Column {
|
||||
onClicked: root.newTabRequested()
|
||||
}
|
||||
}
|
||||
|
||||
DankTooltipV2 {
|
||||
id: tabTooltip
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,19 +168,6 @@ PanelWindow {
|
||||
popupContextMenuLoader.active = false;
|
||||
}
|
||||
|
||||
function dismissPopupReliably() {
|
||||
if (!notificationData || win.exiting || win._isDestroying)
|
||||
return;
|
||||
if (notificationData.timer)
|
||||
notificationData.timer.stop();
|
||||
notificationData.popup = false;
|
||||
// Fallback if wrapperConn.onPopupChanged doesn't reach startExit.
|
||||
Qt.callLater(() => {
|
||||
if (!win.exiting && !win._isDestroying)
|
||||
startExit();
|
||||
});
|
||||
}
|
||||
|
||||
visible: !_finalized
|
||||
WlrLayershell.layer: {
|
||||
const shouldUseOverlay = notificationData && (SettingsData.notificationOverlayEnabled || notificationData.urgency === NotificationUrgency.Critical);
|
||||
@@ -1020,7 +1007,8 @@ PanelWindow {
|
||||
z: 15
|
||||
|
||||
onClicked: {
|
||||
dismissPopupReliably();
|
||||
if (notificationData && !win.exiting)
|
||||
notificationData.popup = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1092,7 +1080,8 @@ PanelWindow {
|
||||
onClicked: {
|
||||
if (modelData && modelData.invoke)
|
||||
modelData.invoke();
|
||||
dismissPopupReliably();
|
||||
if (notificationData && !win.exiting)
|
||||
notificationData.popup = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1174,7 +1163,7 @@ PanelWindow {
|
||||
notificationData.actions[0].invoke();
|
||||
NotificationService.dismissNotification(notificationData);
|
||||
} else {
|
||||
dismissPopupReliably();
|
||||
notificationData.popup = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Widgets
|
||||
|
||||
DankOSD {
|
||||
id: root
|
||||
@@ -186,11 +185,10 @@ DankOSD {
|
||||
visible: false
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
Item {
|
||||
id: blurredBg
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
opacity: 0.7
|
||||
visible: false
|
||||
|
||||
MultiEffect {
|
||||
anchors.centerIn: parent
|
||||
@@ -205,6 +203,24 @@ DankOSD {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bgMask
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: blurredBg
|
||||
maskEnabled: true
|
||||
maskSource: bgMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
@@ -368,7 +368,7 @@ Item {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.deleteVpnConfirm.showWithOptions({
|
||||
deleteVpnConfirm.showWithOptions({
|
||||
title: I18n.tr("Delete VPN"),
|
||||
message: I18n.tr("Delete \"%1\"?").arg(modelData.name),
|
||||
confirmText: I18n.tr("Delete"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
@@ -520,27 +520,28 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
|
||||
ClippingRectangle {
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var wp = Theme.wallpaperPath;
|
||||
if (!wp || wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
|
||||
sourceSize.width: 120
|
||||
sourceSize.height: 120
|
||||
asynchronous: true
|
||||
source: {
|
||||
var wp = Theme.wallpaperPath;
|
||||
if (!wp || wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
|
||||
sourceSize.width: 120
|
||||
sourceSize.height: 120
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: autoWallpaperMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,6 +553,16 @@ Item {
|
||||
visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: autoWallpaperMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "black"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
@@ -75,27 +75,28 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
|
||||
ClippingRectangle {
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var wp = root.currentWallpaper;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
source: {
|
||||
var wp = root.currentWallpaper;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: wallpaperMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +108,16 @@ Item {
|
||||
visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: wallpaperMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "black"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "image"
|
||||
@@ -410,30 +421,31 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
|
||||
ClippingRectangle {
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var wp = SessionData.wallpaperPathLight;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: {
|
||||
var lightWallpaper = SessionData.wallpaperPathLight;
|
||||
return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
|
||||
}
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
source: {
|
||||
var wp = SessionData.wallpaperPathLight;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: {
|
||||
var lightWallpaper = SessionData.wallpaperPathLight;
|
||||
return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
|
||||
}
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: lightMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +463,16 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: lightMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "black"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "light_mode"
|
||||
@@ -589,30 +611,31 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
|
||||
ClippingRectangle {
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var wp = SessionData.wallpaperPathDark;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: {
|
||||
var darkWallpaper = SessionData.wallpaperPathDark;
|
||||
return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
|
||||
}
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
source: {
|
||||
var wp = SessionData.wallpaperPathDark;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: {
|
||||
var darkWallpaper = SessionData.wallpaperPathDark;
|
||||
return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
|
||||
}
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: darkMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,6 +653,16 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: darkMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "black"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "dark_mode"
|
||||
|
||||
@@ -141,37 +141,16 @@ Variants {
|
||||
function invalidate() {
|
||||
_settleFrames = 3;
|
||||
backingWindow?.update();
|
||||
if (!_wedgeBounced)
|
||||
wedgeWatchdog.restart();
|
||||
}
|
||||
|
||||
onRenderActiveChanged: invalidate()
|
||||
onBackingWindowChanged: invalidate()
|
||||
|
||||
// No swap after a requested frame: dropped frame callback left the window
|
||||
// unexposed (qtwayland mFrameCallbackTimedOut); only surface re-attach recovers.
|
||||
property bool _wedgeBounced: false
|
||||
|
||||
Timer {
|
||||
id: wedgeWatchdog
|
||||
interval: 3000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.backingWindow || !wallpaperWindow.visible || IdleService.isShellLocked)
|
||||
return;
|
||||
log.warn("no frame swapped on", modelData.name, "since last invalidate, re-attaching surface");
|
||||
root._wedgeBounced = true;
|
||||
surfaceReattach.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backingWindow
|
||||
function onFrameSwapped() {
|
||||
if (root._settleFrames > 0)
|
||||
root._settleFrames--;
|
||||
root._wedgeBounced = false;
|
||||
wedgeWatchdog.stop();
|
||||
}
|
||||
function onVisibleChanged() {
|
||||
root.invalidate();
|
||||
@@ -197,29 +176,10 @@ Variants {
|
||||
function onWallpaperFillModeChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
function onEffectiveWallpaperBackgroundColorChanged() {
|
||||
function onWallpaperBackgroundColorModeChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onMonitorWallpaperFillModesChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
function onPerMonitorWallpaperChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
// Theme changes repaint DankBackdrop but nothing else wakes the render loop
|
||||
Connections {
|
||||
target: Theme
|
||||
enabled: root.isColorSource || currentWallpaper.status === Image.Error
|
||||
function onPrimaryChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
function onBackgroundChanged() {
|
||||
function onWallpaperBackgroundCustomColorChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
@@ -542,13 +502,13 @@ Variants {
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
invalidate();
|
||||
if (!source || source.startsWith("#")) {
|
||||
setWallpaperImmediate("");
|
||||
return;
|
||||
}
|
||||
|
||||
root.changePending = true;
|
||||
invalidate();
|
||||
|
||||
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
||||
|
||||
@@ -568,14 +528,10 @@ Variants {
|
||||
}
|
||||
|
||||
function setWallpaperImmediate(newSource) {
|
||||
transitionDelayTimer.stop();
|
||||
transitionAnimation.stop();
|
||||
root.transitionProgress = 0.0;
|
||||
root.effectActive = false;
|
||||
root.screenScale = CompositorService.getScreenScale(modelData);
|
||||
// No status change coming to clear the flag
|
||||
if (!newSource || currentWallpaper.source.toString() === newSource)
|
||||
root.changePending = false;
|
||||
currentWallpaper.source = newSource;
|
||||
nextWallpaper.source = "";
|
||||
|
||||
@@ -611,14 +567,10 @@ Variants {
|
||||
}
|
||||
|
||||
function changeWallpaper(newPath, force) {
|
||||
if (!force && newPath === currentWallpaper.source.toString()) {
|
||||
root.changePending = false;
|
||||
if (!force && newPath === currentWallpaper.source)
|
||||
return;
|
||||
}
|
||||
if (!newPath || newPath.startsWith("#")) {
|
||||
root.changePending = false;
|
||||
if (!newPath || newPath.startsWith("#"))
|
||||
return;
|
||||
}
|
||||
root.screenScale = CompositorService.getScreenScale(modelData);
|
||||
if (root.transitioning || root.effectActive) {
|
||||
root.pendingWallpaper = newPath;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
@@ -58,6 +58,23 @@ Item {
|
||||
visible: intersectsViewport
|
||||
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
|
||||
|
||||
Rectangle {
|
||||
id: maskRect
|
||||
width: root.width
|
||||
height: root.height
|
||||
radius: Theme.cornerRadius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: maskRect
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
@@ -87,55 +104,51 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
ScreencopyView {
|
||||
id: windowPreview
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
|
||||
live: true
|
||||
|
||||
ScreencopyView {
|
||||
id: windowPreview
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
|
||||
live: true
|
||||
radius: Theme.cornerRadius
|
||||
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) :
|
||||
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
|
||||
Theme.withAlpha(Theme.surfaceContainer, 0.1)
|
||||
border.color: Theme.withAlpha(Theme.outline, 0.3)
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) : Theme.withAlpha(Theme.surfaceContainer, 0.1)
|
||||
border.color: Theme.withAlpha(Theme.outline, 0.3)
|
||||
border.width: 1
|
||||
}
|
||||
ColumnLayout {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.fontSizeSmall * 0.5
|
||||
|
||||
ColumnLayout {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.fontSizeSmall * 0.5
|
||||
Image {
|
||||
id: windowIcon
|
||||
property var iconSize: {
|
||||
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1)
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: root.iconPath
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
sourceSize: Qt.size(iconSize, iconSize)
|
||||
|
||||
Image {
|
||||
id: windowIcon
|
||||
property var iconSize: {
|
||||
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: root.iconPath
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
sourceSize: Qt.size(iconSize, iconSize)
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,11 +298,7 @@ Singleton {
|
||||
function getBuiltInLauncherItems(pluginId, query) {
|
||||
if (pluginId === "dms_clipboard_search") {
|
||||
const trimmed = (query || "").toString().trim();
|
||||
const entries = ClipboardService.getCachedLauncherSearchEntries(trimmed, 20).slice().sort((a, b) => {
|
||||
if (a.pinned !== b.pinned)
|
||||
return b.pinned ? 1 : -1;
|
||||
return (b.id || 0) - (a.id || 0);
|
||||
});
|
||||
const entries = ClipboardService.internalEntries.length > 0 ? ClipboardService.getLauncherEntries(trimmed, 20, 0) : ClipboardService.getCachedLauncherSearchEntries(trimmed, 20);
|
||||
return entries.map(entry => ({
|
||||
type: "clipboard",
|
||||
data: entry
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user