mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 11:38:30 -04:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 969b780ad1 | |||
| f5dc5061fd | |||
| c44de46022 | |||
| 03ebf6693d | |||
| 1724aedd49 | |||
| d799175c07 | |||
| ee6f7b4798 | |||
| 0511cd19df | |||
| 71b1901ab0 | |||
| 32e2d96e55 | |||
| 647766b9fa | |||
| 0cd8974110 | |||
| 0c4c5fc146 | |||
| b447e16374 | |||
| 0bb8353a33 | |||
| 19a7dcf17d | |||
| 8a1acb63c9 | |||
| 6f298d3f52 | |||
| 3e481a566b |
+5
-2
@@ -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.
|
Command-line interface and daemon for shell management and system control.
|
||||||
|
|
||||||
**dankinstall**
|
**dankinstall**
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## System Integration
|
## System Integration
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
|
|||||||
|
|
||||||
## Supported Distributions
|
## Supported Distributions
|
||||||
|
|
||||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives)
|
||||||
|
|
||||||
**Arch Linux**
|
**Arch Linux**
|
||||||
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
||||||
@@ -214,4 +214,7 @@ Most packages available in standard repos. Minimal building required.
|
|||||||
**Gentoo**
|
**Gentoo**
|
||||||
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
|
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.
|
See installer output for distribution-specific details during installation.
|
||||||
|
|||||||
@@ -1534,6 +1534,8 @@ func packageInstallHint() string {
|
|||||||
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
|
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:
|
default:
|
||||||
return "Run 'dms greeter install' to install greeter"
|
return "Run 'dms greeter install' to install greeter"
|
||||||
}
|
}
|
||||||
@@ -1572,7 +1574,8 @@ func isPackageOnlyGreeterDistro() bool {
|
|||||||
config.Family == distros.FamilySUSE ||
|
config.Family == distros.FamilySUSE ||
|
||||||
config.Family == distros.FamilyUbuntu ||
|
config.Family == distros.FamilyUbuntu ||
|
||||||
config.Family == distros.FamilyFedora ||
|
config.Family == distros.FamilyFedora ||
|
||||||
config.Family == distros.FamilyArch
|
config.Family == distros.FamilyArch ||
|
||||||
|
config.Family == distros.FamilyVoid
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptCompositorChoice(compositors []string) (string, error) {
|
func promptCompositorChoice(compositors []string) (string, error) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"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/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
@@ -298,6 +299,9 @@ func runSetup() error {
|
|||||||
if wmSelected {
|
if wmSelected {
|
||||||
if wm == deps.WindowManagerMango {
|
if wm == deps.WindowManagerMango {
|
||||||
useSystemd = false
|
useSystemd = false
|
||||||
|
} else if isVoidSetup() {
|
||||||
|
useSystemd = false
|
||||||
|
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
|
||||||
} else {
|
} else {
|
||||||
useSystemd = promptSystemd()
|
useSystemd = promptSystemd()
|
||||||
}
|
}
|
||||||
@@ -372,6 +376,15 @@ func runSetup() error {
|
|||||||
return nil
|
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.
|
// Add user to the input group for the evdev manager for inut state tracking.
|
||||||
// Caps Lock OSD and the Caps Lock bar indicator.
|
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||||
func ensureInputGroup() {
|
func ensureInputGroup() {
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
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) {
|
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
|
var results []DeploymentResult
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
|
|||||||
copy-on-select = false
|
copy-on-select = false
|
||||||
confirm-close-surface = false
|
confirm-close-surface = false
|
||||||
|
|
||||||
# Disable annoying copied to clipboard
|
# Disable in-app Ghostty toast notifications
|
||||||
app-notifications = no-clipboard-copy,no-config-reload
|
app-notifications = false
|
||||||
|
|
||||||
# Key bindings for common actions
|
# Key bindings for common actions
|
||||||
#keybind = ctrl+c=copy_to_clipboard
|
#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 + 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 + 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 + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
|
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper"))
|
||||||
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
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 + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
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
|
# Notepad
|
||||||
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
||||||
# Browse Wallpapers
|
# Browse Wallpapers
|
||||||
bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
|
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper
|
||||||
# Power Menu
|
# Power Menu
|
||||||
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
||||||
# Cycle Display Profile
|
# Cycle Display Profile
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ binds {
|
|||||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
}
|
}
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper";
|
||||||
}
|
}
|
||||||
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
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"; }
|
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
|
|||||||
@@ -20,3 +20,10 @@ window-rule {
|
|||||||
tiled-state true
|
tiled-state true
|
||||||
draw-border-with-background false
|
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 dms files
|
||||||
include "dms/colors.kdl"
|
include optional=true "dms/colors.kdl"
|
||||||
include "dms/layout.kdl"
|
include optional=true "dms/layout.kdl"
|
||||||
include "dms/alttab.kdl"
|
include optional=true "dms/alttab.kdl"
|
||||||
include "dms/binds.kdl"
|
include optional=true "dms/binds.kdl"
|
||||||
include "dms/outputs.kdl"
|
include optional=true "dms/outputs.kdl"
|
||||||
include "dms/cursor.kdl"
|
include optional=true "dms/cursor.kdl"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const (
|
|||||||
FamilyDebian DistroFamily = "debian"
|
FamilyDebian DistroFamily = "debian"
|
||||||
FamilyNix DistroFamily = "nix"
|
FamilyNix DistroFamily = "nix"
|
||||||
FamilyGentoo DistroFamily = "gentoo"
|
FamilyGentoo DistroFamily = "gentoo"
|
||||||
|
FamilyVoid DistroFamily = "void"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PackageManagerType defines the package manager a distro uses
|
// PackageManagerType defines the package manager a distro uses
|
||||||
@@ -29,6 +30,7 @@ const (
|
|||||||
PackageManagerZypper PackageManagerType = "zypper"
|
PackageManagerZypper PackageManagerType = "zypper"
|
||||||
PackageManagerNix PackageManagerType = "nix"
|
PackageManagerNix PackageManagerType = "nix"
|
||||||
PackageManagerPortage PackageManagerType = "portage"
|
PackageManagerPortage PackageManagerType = "portage"
|
||||||
|
PackageManagerXBPS PackageManagerType = "xbps"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RepositoryType defines the type of repository for a package
|
// RepositoryType defines the type of repository for a package
|
||||||
@@ -42,6 +44,7 @@ const (
|
|||||||
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
||||||
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||||
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||||
|
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
|
||||||
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,530 @@
|
|||||||
|
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())
|
||||||
|
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
|
||||||
|
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,6 +30,11 @@ const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
|||||||
|
|
||||||
const GreeterCacheDir = "/var/cache/dms-greeter"
|
const GreeterCacheDir = "/var/cache/dms-greeter"
|
||||||
|
|
||||||
|
const (
|
||||||
|
runitSvDir = "/etc/sv"
|
||||||
|
runitServiceDir = "/var/service"
|
||||||
|
)
|
||||||
|
|
||||||
func DetectDMSPath() (string, error) {
|
func DetectDMSPath() (string, error) {
|
||||||
return config.LocateDMSConfig()
|
return config.LocateDMSConfig()
|
||||||
}
|
}
|
||||||
@@ -41,6 +46,96 @@ func IsNixOS() bool {
|
|||||||
return err == nil
|
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 {
|
func DetectGreeterGroup() string {
|
||||||
data, err := os.ReadFile("/etc/group")
|
data, err := os.ReadFile("/etc/group")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -766,6 +861,8 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
|||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
|
||||||
case distros.FamilyGentoo:
|
case distros.FamilyGentoo:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
|
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:
|
case distros.FamilyNix:
|
||||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||||
default:
|
default:
|
||||||
@@ -892,6 +989,14 @@ 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)
|
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")
|
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:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -909,6 +1014,20 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
|||||||
return true
|
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
|
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
||||||
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||||
if IsGreeterPackaged() {
|
if IsGreeterPackaged() {
|
||||||
@@ -2275,6 +2394,19 @@ func checkSystemdEnabled(service string) (string, error) {
|
|||||||
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
|
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
|
||||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||||
for _, dm := range conflictingDMs {
|
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)
|
state, err := checkSystemdEnabled(dm)
|
||||||
if err != nil || state == "" || state == "not-found" {
|
if err != nil || state == "" || state == "not-found" {
|
||||||
continue
|
continue
|
||||||
@@ -2294,6 +2426,19 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
|
|||||||
|
|
||||||
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
|
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
|
||||||
func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
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")
|
state, err := checkSystemdEnabled("greetd")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check greetd state: %w", err)
|
return fmt.Errorf("failed to check greetd state: %w", err)
|
||||||
@@ -2317,6 +2462,11 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func EnsureGraphicalTarget(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")
|
cmd := exec.Command("systemctl", "get-default")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -236,13 +236,18 @@ func (r *Runner) Run() error {
|
|||||||
r.log("Starting configuration deployment")
|
r.log("Starting configuration deployment")
|
||||||
|
|
||||||
deployer := config.NewConfigDeployer(r.logChan)
|
deployer := config.NewConfigDeployer(r.logChan)
|
||||||
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
|
useSystemd := true
|
||||||
|
if distroConfig, exists := distros.Registry[osInfo.Distribution.ID]; exists && distroConfig.Family == distros.FamilyVoid {
|
||||||
|
useSystemd = false
|
||||||
|
}
|
||||||
|
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
wm,
|
wm,
|
||||||
terminal,
|
terminal,
|
||||||
dependencies,
|
dependencies,
|
||||||
replaceConfigs,
|
replaceConfigs,
|
||||||
reinstallItems,
|
reinstallItems,
|
||||||
|
useSystemd,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configuration deployment failed: %w", err)
|
return fmt.Errorf("configuration deployment failed: %w", err)
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ func GetQtLoggingRules() string {
|
|||||||
level = "info"
|
level = "info"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
|
||||||
|
// probes); suppressed except at debug level
|
||||||
var rules []string
|
var rules []string
|
||||||
switch strings.ToLower(level) {
|
switch strings.ToLower(level) {
|
||||||
case "fatal":
|
case "fatal":
|
||||||
@@ -75,13 +77,13 @@ func GetQtLoggingRules() string {
|
|||||||
case "error":
|
case "error":
|
||||||
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
|
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
|
||||||
case "warn", "warning":
|
case "warn", "warning":
|
||||||
rules = []string{"*.debug=false", "*.info=false"}
|
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"}
|
||||||
case "info":
|
case "info":
|
||||||
rules = []string{"*.debug=false"}
|
rules = []string{"*.debug=false", "scene.warning=false"}
|
||||||
case "debug":
|
case "debug":
|
||||||
return ""
|
return ""
|
||||||
default:
|
default:
|
||||||
rules = []string{"*.debug=false"}
|
rules = []string{"*.debug=false", "scene.warning=false"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(rules, ";")
|
return strings.Join(rules, ";")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ func (m Model) deployConfigurations() tea.Cmd {
|
|||||||
|
|
||||||
deployer := config.NewConfigDeployer(m.logChan)
|
deployer := config.NewConfigDeployer(m.logChan)
|
||||||
|
|
||||||
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems)
|
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems, m.useSystemdConfig())
|
||||||
|
|
||||||
return configDeploymentResult{
|
return configDeploymentResult{
|
||||||
results: results,
|
results: results,
|
||||||
@@ -138,6 +139,17 @@ func (m Model) deployConfigurations() tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (m Model) viewConfigConfirmation() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
@@ -195,12 +207,50 @@ func (m Model) viewConfigConfirmation() string {
|
|||||||
b.WriteString(backup)
|
b.WriteString(backup)
|
||||||
b.WriteString("\n\n")
|
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")
|
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue")
|
||||||
b.WriteString(help)
|
b.WriteString(help)
|
||||||
|
|
||||||
return b.String()
|
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) {
|
func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if result, ok := msg.(configCheckResult); ok {
|
if result, ok := msg.(configCheckResult); ok {
|
||||||
if result.error != nil {
|
if result.error != nil {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
|||||||
return "dms-shell-git"
|
return "dms-shell-git"
|
||||||
}
|
}
|
||||||
return "dms-shell"
|
return "dms-shell"
|
||||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE, distros.FamilyVoid:
|
||||||
if isGit {
|
if isGit {
|
||||||
return "dms-git"
|
return "dms-git"
|
||||||
}
|
}
|
||||||
@@ -168,6 +168,8 @@ func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
|
|||||||
return "sudo apt remove " + pkg
|
return "sudo apt remove " + pkg
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
return "sudo zypper remove " + pkg
|
return "sudo zypper remove " + pkg
|
||||||
|
case distros.FamilyVoid:
|
||||||
|
return "sudo xbps-remove -R " + pkg
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -201,8 +203,18 @@ func (m Model) viewInstallComplete() string {
|
|||||||
|
|
||||||
wm := m.selectedWindowManager()
|
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\""
|
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 {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
loginHint = "If you do not have a greeter, login with \"niri-session\""
|
loginHint = "If you do not have a greeter, login with \"niri-session\""
|
||||||
@@ -211,6 +223,7 @@ func (m Model) viewInstallComplete() string {
|
|||||||
case deps.WindowManagerMango:
|
case deps.WindowManagerMango:
|
||||||
loginHint = "If you do not have a greeter, login with \"mango\""
|
loginHint = "If you do not have a greeter, login with \"mango\""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\n" + loginHint)
|
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\n" + loginHint)
|
||||||
@@ -222,7 +235,17 @@ func (m Model) viewInstallComplete() string {
|
|||||||
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
|
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
|
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
|
||||||
if wm == deps.WindowManagerMango {
|
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 {
|
||||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
|
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")
|
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ in {
|
|||||||
text = lib.pipe cfg'.filesToInclude [
|
text = lib.pipe cfg'.filesToInclude [
|
||||||
(map (filename: "dms/${filename}"))
|
(map (filename: "dms/${filename}"))
|
||||||
withOriginalConfig
|
withOriginalConfig
|
||||||
(map (filename: "include \"${filename}.kdl\""))
|
(map (filename: "include optional=true \"${filename}.kdl\""))
|
||||||
(files: files ++ fixes)
|
(files: files ++ fixes)
|
||||||
(builtins.concatStringsSep "\n")
|
(builtins.concatStringsSep "\n")
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
let
|
let
|
||||||
homeManagerNixosModule =
|
homeManagerNixosModule =
|
||||||
(fetchTarball {
|
(fetchTarball {
|
||||||
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
|
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
|
||||||
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
|
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
|
||||||
})
|
})
|
||||||
+ "/nixos";
|
+ "/nixos";
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
let
|
let
|
||||||
homeManagerNixosModule =
|
homeManagerNixosModule =
|
||||||
(fetchTarball {
|
(fetchTarball {
|
||||||
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
|
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
|
||||||
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
|
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
|
||||||
})
|
})
|
||||||
+ "/nixos";
|
+ "/nixos";
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@ pkgs.testers.runNixOSTest {
|
|||||||
machine.wait_for_unit("multi-user.target")
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
machine.succeed("su -- danklinux -c 'test -f ~/.config/niri/config.kdl'")
|
machine.succeed("su -- danklinux -c 'test -f ~/.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 optional=true \\\"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 \"include optional=true \\\"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 \"spawn-at-startup\" ~/.config/niri/hm.kdl'")
|
||||||
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
|
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
|
||||||
'';
|
'';
|
||||||
|
|||||||
+23
-2
@@ -52,8 +52,8 @@ checkout at `srcpkgs/<pkg>/template` to build or submit it.
|
|||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`,
|
Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`,
|
||||||
and `matugen` (which drives the Material You theming). The rest are optional —
|
`matugen` (which drives the Material You theming), `dbus`, and `elogind`.
|
||||||
install whichever features you want:
|
The rest are optional, install whichever features you want:
|
||||||
|
|
||||||
| Package | Enables |
|
| Package | Enables |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -98,6 +98,27 @@ spawn-at-startup "dms" "run"
|
|||||||
|
|
||||||
or Hyprland: `exec-once = 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)
|
## Greeter (optional)
|
||||||
|
|
||||||
Install `dms-greeter`, then let the CLI do the setup:
|
Install `dms-greeter`, then let the CLI do the setup:
|
||||||
|
|||||||
@@ -32,13 +32,12 @@ conflicts="dms"
|
|||||||
provides="dms-${version}_${revision}"
|
provides="dms-${version}_${revision}"
|
||||||
|
|
||||||
# Optional feature deps are listed in distro/void/README.md.
|
# Optional feature deps are listed in distro/void/README.md.
|
||||||
depends="quickshell accountsservice dgop matugen dbus"
|
depends="quickshell accountsservice dgop matugen dbus elogind"
|
||||||
|
|
||||||
post_install() {
|
post_install() {
|
||||||
# QML shell tree (build_style=go already installed the dms binary)
|
# QML shell tree (build_style=go already installed the dms binary)
|
||||||
vmkdir usr/share/quickshell/dms
|
vmkdir usr/share/quickshell/dms
|
||||||
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
||||||
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
|
|
||||||
|
|
||||||
# Desktop entry + icon
|
# Desktop entry + icon
|
||||||
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
||||||
|
|||||||
@@ -22,13 +22,12 @@ distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v$
|
|||||||
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
|
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
|
||||||
|
|
||||||
# Optional feature deps are listed in distro/void/README.md.
|
# Optional feature deps are listed in distro/void/README.md.
|
||||||
depends="quickshell accountsservice dgop matugen dbus"
|
depends="quickshell accountsservice dgop matugen dbus elogind"
|
||||||
|
|
||||||
post_install() {
|
post_install() {
|
||||||
# QML shell tree (build_style=go already installed the dms binary)
|
# QML shell tree (build_style=go already installed the dms binary)
|
||||||
vmkdir usr/share/quickshell/dms
|
vmkdir usr/share/quickshell/dms
|
||||||
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
||||||
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
|
|
||||||
|
|
||||||
# Desktop entry + icon
|
# Desktop entry + icon
|
||||||
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
||||||
|
|||||||
Generated
+3
-3
@@ -18,11 +18,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778443072,
|
"lastModified": 1783224372,
|
||||||
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
|
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
|
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -120,12 +120,18 @@ Singleton {
|
|||||||
|
|
||||||
function setMonitorScrollPosition(screenName, scrollX, scrollY) {
|
function setMonitorScrollPosition(screenName, scrollX, scrollY) {
|
||||||
var newPositions = Object.assign({}, monitorScrollPositions);
|
var newPositions = Object.assign({}, monitorScrollPositions);
|
||||||
newPositions[screenName] = { scrollX: scrollX, scrollY: scrollY };
|
newPositions[screenName] = {
|
||||||
|
scrollX: scrollX,
|
||||||
|
scrollY: scrollY
|
||||||
|
};
|
||||||
monitorScrollPositions = newPositions;
|
monitorScrollPositions = newPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMonitorScrollPosition(screenName) {
|
function getMonitorScrollPosition(screenName) {
|
||||||
return monitorScrollPositions[screenName] || { scrollX: 50, scrollY: 50 };
|
return monitorScrollPositions[screenName] || {
|
||||||
|
scrollX: 50,
|
||||||
|
scrollY: 50
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearMonitorScrollPosition(screenName) {
|
function clearMonitorScrollPosition(screenName) {
|
||||||
@@ -209,6 +215,8 @@ Singleton {
|
|||||||
property string locale: ""
|
property string locale: ""
|
||||||
property string timeLocale: ""
|
property string timeLocale: ""
|
||||||
|
|
||||||
|
property string notepadLastMode: ""
|
||||||
|
|
||||||
property string launcherLastMode: "all"
|
property string launcherLastMode: "all"
|
||||||
property string launcherLastFileSearchType: "all"
|
property string launcherLastFileSearchType: "all"
|
||||||
property string launcherLastQuery: ""
|
property string launcherLastQuery: ""
|
||||||
@@ -1216,6 +1224,13 @@ Singleton {
|
|||||||
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setNotepadLastMode(mode) {
|
||||||
|
if (notepadLastMode === mode)
|
||||||
|
return;
|
||||||
|
notepadLastMode = mode;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function setLauncherLastMode(mode) {
|
function setLauncherLastMode(mode) {
|
||||||
launcherLastMode = mode;
|
launcherLastMode = mode;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ var SPEC = {
|
|||||||
locale: { def: "", onChange: "updateLocale" },
|
locale: { def: "", onChange: "updateLocale" },
|
||||||
timeLocale: { def: "" },
|
timeLocale: { def: "" },
|
||||||
|
|
||||||
|
notepadLastMode: { def: "" },
|
||||||
|
|
||||||
launcherLastMode: { def: "all" },
|
launcherLastMode: { def: "all" },
|
||||||
launcherLastFileSearchType: { def: "all" },
|
launcherLastFileSearchType: { def: "all" },
|
||||||
launcherLastQuery: { def: "" },
|
launcherLastQuery: { def: "" },
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open(): string {
|
function open(): string {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (PopoutService.notepadResolvedMode === "popout") {
|
||||||
PopoutService.openNotepadPopout();
|
PopoutService.openNotepadPopout();
|
||||||
return "NOTEPAD_OPEN_SUCCESS";
|
return "NOTEPAD_OPEN_SUCCESS";
|
||||||
}
|
}
|
||||||
@@ -388,7 +388,7 @@ Item {
|
|||||||
function openFile(path: string): string {
|
function openFile(path: string): string {
|
||||||
if (!path)
|
if (!path)
|
||||||
return open();
|
return open();
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (PopoutService.notepadResolvedMode === "popout") {
|
||||||
PopoutService.openNotepadPopoutWithFile(path);
|
PopoutService.openNotepadPopoutWithFile(path);
|
||||||
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
||||||
}
|
}
|
||||||
@@ -402,7 +402,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function close(): string {
|
function close(): string {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (PopoutService.notepadResolvedMode === "popout") {
|
||||||
PopoutService.notepadPopout?.hide();
|
PopoutService.notepadPopout?.hide();
|
||||||
return "NOTEPAD_CLOSE_SUCCESS";
|
return "NOTEPAD_CLOSE_SUCCESS";
|
||||||
}
|
}
|
||||||
@@ -415,7 +415,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle(): string {
|
function toggle(): string {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (PopoutService.notepadResolvedMode === "popout") {
|
||||||
PopoutService.toggleNotepadPopout();
|
PopoutService.toggleNotepadPopout();
|
||||||
return "NOTEPAD_TOGGLE_SUCCESS";
|
return "NOTEPAD_TOGGLE_SUCCESS";
|
||||||
}
|
}
|
||||||
@@ -712,12 +712,13 @@ Item {
|
|||||||
target: "hypr"
|
target: "hypr"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! TODO - remove for v1.6
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function wallpaper(): string {
|
function wallpaper(): string {
|
||||||
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
||||||
if (bar) {
|
if (bar) {
|
||||||
bar.triggerWallpaperBrowser();
|
bar.triggerWallpaperBrowser();
|
||||||
return "SUCCESS: Toggled wallpaper browser";
|
return "WARN; deprecated, use dms ipc call dash toggle wallpaper instead";
|
||||||
}
|
}
|
||||||
return "ERROR: Failed to toggle wallpaper browser";
|
return "ERROR: Failed to toggle wallpaper browser";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Column {
|
|||||||
StyledText {
|
StyledText {
|
||||||
id: codenameText
|
id: codenameText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "Saffron Bloom"
|
text: "The Wolverine"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
@@ -74,7 +74,7 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: "New launcher, enhanced plugin system, KDE Connect, & more"
|
text: "Frame Mode, DankCalendar, Spotlight, & more"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
@@ -108,77 +108,99 @@ Column {
|
|||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "space_dashboard"
|
iconName: "border_outer"
|
||||||
title: "Dank Launcher V2"
|
title: "Frame Mode"
|
||||||
description: "New capabilities & plugins"
|
description: "Connected shell surfaces"
|
||||||
onClicked: PopoutService.openDankLauncherV2()
|
onClicked: PopoutService.openSettingsWithTab("frame")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "smartphone"
|
iconName: "calendar_month"
|
||||||
title: "Phone Connect"
|
title: "DankCalendar"
|
||||||
description: "KDE Connect & Valent"
|
description: "Native calendar & events"
|
||||||
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
|
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dankcalendar")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "monitor_heart"
|
iconName: "search"
|
||||||
title: "System Monitor"
|
title: "Spotlight"
|
||||||
description: "Redesigned process list"
|
description: "Lightweight launcher"
|
||||||
onClicked: PopoutService.showProcessListModal()
|
onClicked: PopoutService.openSpotlightBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "window"
|
iconName: "window"
|
||||||
title: "Window Rules"
|
title: "Window Rules"
|
||||||
description: "niri window rule manager"
|
description: "Rules for all compositors"
|
||||||
visible: CompositorService.isNiri
|
|
||||||
onClicked: PopoutService.openSettingsWithTab("window_rules")
|
onClicked: PopoutService.openSettingsWithTab("window_rules")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "notifications_active"
|
iconName: "display_settings"
|
||||||
title: "Enhanced Notifications"
|
title: "Display Profiles"
|
||||||
description: "Configurable rules & styling"
|
description: "Auto-switch monitor layouts"
|
||||||
visible: !CompositorService.isNiri
|
onClicked: PopoutService.openSettingsWithTab("display_config")
|
||||||
onClicked: PopoutService.openSettingsWithTab("notifications")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "dock_to_bottom"
|
iconName: "dvr"
|
||||||
title: "Dock Enhancements"
|
title: "Multiplexer Launcher"
|
||||||
description: "Bar dock widget & more"
|
description: "Attach to tmux sessions"
|
||||||
onClicked: PopoutService.openSettingsWithTab("dock")
|
onClicked: PopoutService.openSettingsWithTab("multiplexers")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "volume_up"
|
iconName: "edit_note"
|
||||||
title: "Audio Aliases"
|
title: "Notepad Rewrite"
|
||||||
description: "Custom device names"
|
description: "Popout & tiling support"
|
||||||
onClicked: PopoutService.openSettingsWithTab("audio")
|
onClicked: PopoutService.openNotepad()
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "extension"
|
iconName: "gradient"
|
||||||
title: "Enhanced Plugin System"
|
title: "M3 Shadows"
|
||||||
description: "Enables new types of plugins"
|
description: "Reworked elevation system"
|
||||||
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")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,12 +252,7 @@ Column {
|
|||||||
|
|
||||||
ChangelogUpgradeNote {
|
ChangelogUpgradeNote {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
|
text: "App ID changed to com.danklinux.dms — update any compositor window rules targeting the old ID"
|
||||||
}
|
|
||||||
|
|
||||||
ChangelogUpgradeNote {
|
|
||||||
width: parent.width
|
|
||||||
text: "Plugin API updated — third-party plugins may need updates"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ FloatingWindow {
|
|||||||
iconName: "open_in_new"
|
iconName: "open_in_new"
|
||||||
backgroundColor: Theme.surfaceContainerHighest
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
textColor: Theme.surfaceText
|
textColor: Theme.surfaceText
|
||||||
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
|
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-5-release")
|
||||||
}
|
}
|
||||||
|
|
||||||
DankButton {
|
DankButton {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
ClippingRectangle {
|
||||||
id: thumbnail
|
id: thumbnail
|
||||||
readonly property var log: Log.scoped("ClipboardThumbnail")
|
readonly property var log: Log.scoped("ClipboardThumbnail")
|
||||||
|
|
||||||
@@ -15,6 +15,10 @@ Item {
|
|||||||
required property int itemIndex
|
required property int itemIndex
|
||||||
property bool disposed: false
|
property bool disposed: false
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
color: "transparent"
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: thumbnailImage
|
id: thumbnailImage
|
||||||
|
|
||||||
@@ -34,7 +38,7 @@ Item {
|
|||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: false
|
cache: false
|
||||||
visible: false
|
visible: entryType === "image" && status === Image.Ready && source != ""
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
sourceSize.width: 128
|
sourceSize.width: 128
|
||||||
sourceSize.height: 128
|
sourceSize.height: 128
|
||||||
@@ -236,33 +240,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
DankIcon {
|
||||||
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ Item {
|
|||||||
|
|
||||||
property bool animationsEnabled: true
|
property bool animationsEnabled: true
|
||||||
|
|
||||||
|
function _kickBlurCommit() {
|
||||||
|
if (typeof contentWindow.update === "function")
|
||||||
|
contentWindow.update();
|
||||||
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
@@ -201,6 +206,12 @@ Item {
|
|||||||
}
|
}
|
||||||
})(), dpr)
|
})(), dpr)
|
||||||
|
|
||||||
|
onAlignedXChanged: _kickBlurCommit()
|
||||||
|
onAlignedYChanged: _kickBlurCommit()
|
||||||
|
onAlignedWidthChanged: _kickBlurCommit()
|
||||||
|
onAlignedHeightChanged: _kickBlurCommit()
|
||||||
|
onShouldBeVisibleChanged: _kickBlurCommit()
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: clickCatcher
|
id: clickCatcher
|
||||||
visible: false
|
visible: false
|
||||||
@@ -244,11 +255,13 @@ Item {
|
|||||||
WindowBlur {
|
WindowBlur {
|
||||||
targetWindow: contentWindow
|
targetWindow: contentWindow
|
||||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
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.
|
// Blur tracks the surface's scaled rect, matching the connected backend.
|
||||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||||
blurWidth: root.shouldBeVisible ? modalContainer.width * s : 0
|
blurWidth: root.shouldBeVisible ? modalContainer.width * visibleScale : 0
|
||||||
blurHeight: root.shouldBeVisible ? modalContainer.height * s : 0
|
blurHeight: root.shouldBeVisible ? modalContainer.height * visibleScale : 0
|
||||||
blurRadius: root.cornerRadius
|
blurRadius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +351,7 @@ Item {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: morph
|
id: morph
|
||||||
property real openProgress: root.shouldBeVisible ? 1 : 0
|
property real openProgress: root.shouldBeVisible ? 1 : 0
|
||||||
|
onOpenProgressChanged: root._kickBlurCommit()
|
||||||
Behavior on openProgress {
|
Behavior on openProgress {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
DankAnim {
|
DankAnim {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Rectangle {
|
|||||||
property string cachedMimeType: ""
|
property string cachedMimeType: ""
|
||||||
property var _requestedEntryId: null
|
property var _requestedEntryId: null
|
||||||
|
|
||||||
readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
|
readonly property bool canLoadImage: typeof entry?.id === "number" && !!entry?.isImage && String(entry?.mimeType ?? "").startsWith("image/")
|
||||||
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
|
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
|
||||||
|
|
||||||
radius: Math.max(6, Theme.cornerRadius - 2)
|
radius: Math.max(6, Theme.cornerRadius - 2)
|
||||||
@@ -41,13 +41,18 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reloadPreview() {
|
function reloadPreview() {
|
||||||
|
if (!canLoadImage || typeof entry?.id !== "number") {
|
||||||
|
_requestedEntryId = null;
|
||||||
cachedImageData = "";
|
cachedImageData = "";
|
||||||
cachedMimeType = "";
|
cachedMimeType = "";
|
||||||
if (!canLoadImage || !entry?.id) {
|
|
||||||
_requestedEntryId = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Entry objects are rebuilt per search; same id means same content
|
||||||
|
if (entry.id === _requestedEntryId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cachedImageData = "";
|
||||||
|
cachedMimeType = "";
|
||||||
const entryId = entry.id;
|
const entryId = entry.id;
|
||||||
_requestedEntryId = entryId;
|
_requestedEntryId = entryId;
|
||||||
DMSService.sendRequest("clipboard.getEntry", {
|
DMSService.sendRequest("clipboard.getEntry", {
|
||||||
@@ -55,17 +60,22 @@ Rectangle {
|
|||||||
}, function (response) {
|
}, function (response) {
|
||||||
if (_requestedEntryId !== entryId)
|
if (_requestedEntryId !== entryId)
|
||||||
return;
|
return;
|
||||||
if (response.error)
|
if (response.error) {
|
||||||
|
_requestedEntryId = null;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
if (!response.result) {
|
if (!response.result) {
|
||||||
|
_requestedEntryId = null;
|
||||||
ClipboardService.refresh();
|
ClipboardService.refresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = response.result;
|
const result = response.result;
|
||||||
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
|
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
|
||||||
const data = (result.data ?? "").toString();
|
const data = (result.data ?? "").toString();
|
||||||
if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
|
if (data.length === 0 || !resolvedSourceUrl(data, mimeType)) {
|
||||||
|
_requestedEntryId = null;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
cachedMimeType = mimeType;
|
cachedMimeType = mimeType;
|
||||||
cachedImageData = data;
|
cachedImageData = data;
|
||||||
});
|
});
|
||||||
@@ -78,6 +88,8 @@ Rectangle {
|
|||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: false
|
cache: false
|
||||||
smooth: true
|
smooth: true
|
||||||
|
sourceSize.width: 128
|
||||||
|
sourceSize.height: 128
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
visible: status === Image.Ready
|
visible: status === Image.Ready
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,15 +50,15 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (!active) {
|
ClipboardService.invalidateLauncherSearchCache();
|
||||||
SessionData.addLauncherHistory(searchQuery);
|
if (active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SessionData.addLauncherHistory(searchQuery);
|
||||||
sections = [];
|
sections = [];
|
||||||
flatModel = [];
|
flatModel = [];
|
||||||
selectedItem = null;
|
selectedItem = null;
|
||||||
_clearModeCache();
|
_clearModeCache();
|
||||||
ClipboardService.invalidateLauncherSearchCache();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchModeChanged: {
|
onSearchModeChanged: {
|
||||||
@@ -110,7 +110,7 @@ Item {
|
|||||||
if (query !== effectiveQuery)
|
if (query !== effectiveQuery)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
searchDebounce.restart();
|
root.requestSearch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +276,23 @@ Item {
|
|||||||
property string appCategory: ""
|
property string appCategory: ""
|
||||||
property var appCategories: []
|
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) {
|
function getSectionViewMode(sectionId) {
|
||||||
if (sectionId === "browse_plugins")
|
if (sectionId === "browse_plugins")
|
||||||
return "list";
|
return "list";
|
||||||
|
var builtInPref = builtInSectionViewPref(sectionId);
|
||||||
|
if (builtInPref?.enforced)
|
||||||
|
return builtInPref.mode;
|
||||||
if (pluginViewPreferences[sectionId]?.enforced)
|
if (pluginViewPreferences[sectionId]?.enforced)
|
||||||
return pluginViewPreferences[sectionId].mode;
|
return pluginViewPreferences[sectionId].mode;
|
||||||
if (sectionViewModes[sectionId])
|
if (sectionViewModes[sectionId])
|
||||||
@@ -302,6 +316,8 @@ Item {
|
|||||||
function setSectionViewMode(sectionId, mode) {
|
function setSectionViewMode(sectionId, mode) {
|
||||||
if (sectionId === "browse_plugins")
|
if (sectionId === "browse_plugins")
|
||||||
return;
|
return;
|
||||||
|
if (builtInSectionViewPref(sectionId)?.enforced)
|
||||||
|
return;
|
||||||
if (pluginViewPreferences[sectionId]?.enforced)
|
if (pluginViewPreferences[sectionId]?.enforced)
|
||||||
return;
|
return;
|
||||||
sectionViewModes = Object.assign({}, sectionViewModes, {
|
sectionViewModes = Object.assign({}, sectionViewModes, {
|
||||||
@@ -325,6 +341,8 @@ Item {
|
|||||||
function canChangeSectionViewMode(sectionId) {
|
function canChangeSectionViewMode(sectionId) {
|
||||||
if (sectionId === "browse_plugins")
|
if (sectionId === "browse_plugins")
|
||||||
return false;
|
return false;
|
||||||
|
if (builtInSectionViewPref(sectionId)?.enforced)
|
||||||
|
return false;
|
||||||
return !pluginViewPreferences[sectionId]?.enforced;
|
return !pluginViewPreferences[sectionId]?.enforced;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,10 +398,29 @@ Item {
|
|||||||
property bool _pluginPhaseForceFirst: false
|
property bool _pluginPhaseForceFirst: false
|
||||||
property var _phase1Items: []
|
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 {
|
Timer {
|
||||||
id: searchDebounce
|
id: searchDebounce
|
||||||
interval: 60
|
interval: 60
|
||||||
onTriggered: root.performSearch()
|
onTriggered: {
|
||||||
|
if (!root._searchPending)
|
||||||
|
return;
|
||||||
|
root._searchPending = false;
|
||||||
|
root.performSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -409,7 +446,7 @@ Item {
|
|||||||
_phase1Items = [];
|
_phase1Items = [];
|
||||||
pluginPhaseTimer.stop();
|
pluginPhaseTimer.stop();
|
||||||
searchQuery = query;
|
searchQuery = query;
|
||||||
searchDebounce.restart();
|
requestSearch();
|
||||||
|
|
||||||
if (searchMode !== "plugins" && query.startsWith("/")) {
|
if (searchMode !== "plugins" && query.startsWith("/")) {
|
||||||
var prefix = Utils.parseFileSearchPrefix(query);
|
var prefix = Utils.parseFileSearchPrefix(query);
|
||||||
@@ -694,6 +731,7 @@ Item {
|
|||||||
if (triggerMatch.isBuiltIn) {
|
if (triggerMatch.isBuiltIn) {
|
||||||
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
|
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
|
||||||
for (var j = 0; j < builtInItems.length; j++) {
|
for (var j = 0; j < builtInItems.length; j++) {
|
||||||
|
builtInItems[j]._preScored = 1000 - j;
|
||||||
allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId));
|
allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1198,8 +1236,12 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformBuiltInSearchItem(item, pluginId) {
|
function transformBuiltInSearchItem(item, pluginId) {
|
||||||
if (pluginId === "dms_clipboard_search" || item.type === "clipboard")
|
if (pluginId === "dms_clipboard_search" || item.type === "clipboard") {
|
||||||
return transformClipboardEntry(item.data || item);
|
var transformed = transformClipboardEntry(item.data || item);
|
||||||
|
if (item._preScored !== undefined)
|
||||||
|
transformed._preScored = item._preScored;
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
return transformBuiltInLauncherItem(item, pluginId);
|
return transformBuiltInLauncherItem(item, pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1871,7 +1913,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function executeSelected() {
|
function executeSelected() {
|
||||||
if (searchDebounce.running) {
|
if (_searchPending) {
|
||||||
|
_searchPending = false;
|
||||||
searchDebounce.stop();
|
searchDebounce.stop();
|
||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,18 @@ Item {
|
|||||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
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
|
signal dialogClosed
|
||||||
|
|
||||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
@@ -328,6 +340,7 @@ Item {
|
|||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
|
id: launcherBlur
|
||||||
targetWindow: launcherWindow
|
targetWindow: launcherWindow
|
||||||
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
||||||
blurX: modalContainer.x
|
blurX: modalContainer.x
|
||||||
@@ -337,6 +350,9 @@ Item {
|
|||||||
blurRadius: root.cornerRadius
|
blurRadius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onWidthChanged: root._kickBlurCommit()
|
||||||
|
onHeightChanged: root._kickBlurCommit()
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
@@ -421,6 +437,9 @@ Item {
|
|||||||
|
|
||||||
opacity: contentVisible ? 1 : 0
|
opacity: contentVisible ? 1 : 0
|
||||||
|
|
||||||
|
onOpacityChanged: root._kickBlurCommit()
|
||||||
|
onSlideOffsetChanged: root._kickBlurCommit()
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: contentVisible ? root._openDuration : root._closeDuration
|
duration: contentVisible ? root._openDuration : root._closeDuration
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ Item {
|
|||||||
readonly property real windowWidth: alignedWidth + contentX + shadowPad
|
readonly property real windowWidth: alignedWidth + contentX + shadowPad
|
||||||
readonly property real windowHeight: alignedHeight + contentY + 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 color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
readonly property bool useBackgroundDarken: !FrameTransitionState.effectiveFrameEnabled && SettingsData.modalDarkenBackground
|
readonly property bool useBackgroundDarken: !FrameTransitionState.effectiveFrameEnabled && SettingsData.modalDarkenBackground
|
||||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
||||||
@@ -113,6 +119,11 @@ Item {
|
|||||||
|
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
|
|
||||||
|
function _kickBlurCommit() {
|
||||||
|
if (typeof launcherWindow.update === "function")
|
||||||
|
launcherWindow.update();
|
||||||
|
}
|
||||||
|
|
||||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
_pendingQuery = query || "";
|
_pendingQuery = query || "";
|
||||||
_pendingMode = mode || "";
|
_pendingMode = mode || "";
|
||||||
@@ -366,11 +377,13 @@ Item {
|
|||||||
WindowBlur {
|
WindowBlur {
|
||||||
targetWindow: launcherWindow
|
targetWindow: launcherWindow
|
||||||
readonly property real s: Math.min(1, modalContainer.publishedScale)
|
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
|
// Blur tracks the surface's scaled rect
|
||||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
|
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5
|
||||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
|
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5
|
||||||
blurWidth: contentVisible ? modalContainer.width * s : 0
|
blurWidth: contentVisible ? modalContainer.width * visibleScale : 0
|
||||||
blurHeight: contentVisible ? modalContainer.height * s : 0
|
blurHeight: contentVisible ? modalContainer.height * visibleScale : 0
|
||||||
blurRadius: root.cornerRadius
|
blurRadius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +473,8 @@ Item {
|
|||||||
opacity: contentVisible ? 1 : 0
|
opacity: contentVisible ? 1 : 0
|
||||||
scale: contentVisible ? 1 : 0.96
|
scale: contentVisible ? 1 : 0.96
|
||||||
transformOrigin: Item.Center
|
transformOrigin: Item.Center
|
||||||
|
onOpacityChanged: root._kickBlurCommit()
|
||||||
|
onPublishedScaleChanged: root._kickBlurCommit()
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Rectangle {
|
|||||||
return item.icon || "";
|
return item.icon || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
|
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/")
|
||||||
|
|
||||||
width: parent?.width ?? 200
|
width: parent?.width ?? 200
|
||||||
height: 52
|
height: 52
|
||||||
|
|||||||
@@ -48,9 +48,8 @@ Rectangle {
|
|||||||
return "file://" + raw;
|
return "file://" + raw;
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
|
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/")
|
||||||
readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
|
readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
|
||||||
readonly property bool previewAnimated: previewSource.toLowerCase().indexOf(".gif") >= 0
|
|
||||||
|
|
||||||
readonly property string typeLabel: {
|
readonly property string typeLabel: {
|
||||||
if (!item)
|
if (!item)
|
||||||
@@ -284,16 +283,10 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: root.previewSource
|
source: root.previewSource
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
sourceSize.width: 128
|
||||||
|
sourceSize.height: 128
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
visible: !root.hasClipboardPreview && !root.previewAnimated
|
visible: !root.hasClipboardPreview
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: root.previewSource
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
playing: visible
|
|
||||||
visible: !root.hasClipboardPreview && root.previewAnimated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipboardLauncherPreview {
|
ClipboardLauncherPreview {
|
||||||
|
|||||||
@@ -165,6 +165,9 @@ Rectangle {
|
|||||||
anchors.margins: root.hasScreencopy ? 4 : 0
|
anchors.margins: root.hasScreencopy ? 4 : 0
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
source: root.item?.data?.attribution || ""
|
source: root.item?.data?.attribution || ""
|
||||||
|
asynchronous: true
|
||||||
|
sourceSize.width: 80
|
||||||
|
sourceSize.height: 80
|
||||||
mipmap: true
|
mipmap: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -98,27 +98,27 @@ StyledRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property string _videoThumb: ""
|
property string _videoThumb: ""
|
||||||
|
property bool _thumbGenAttempted: false
|
||||||
|
|
||||||
|
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||||
onVideoThumbnailPathChanged: {
|
onVideoThumbnailPathChanged: {
|
||||||
_videoThumb = "";
|
_thumbGenAttempted = false;
|
||||||
if (!videoThumbnailPath)
|
_videoThumb = videoThumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVideoThumbnail() {
|
||||||
|
if (_thumbGenAttempted)
|
||||||
return;
|
return;
|
||||||
|
_thumbGenAttempted = true;
|
||||||
|
_videoThumb = "";
|
||||||
const thumbPath = videoThumbnailPath;
|
const thumbPath = videoThumbnailPath;
|
||||||
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||||
const size = _thumbnailPx;
|
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s " + _thumbnailPx + " -f";
|
||||||
const fp = delegateRoot.filePath;
|
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, delegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||||
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)
|
if (exitCode === 0)
|
||||||
_videoThumb = thumbPath;
|
_videoThumb = thumbPath;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconForFile(fileName) {
|
function getIconForFile(fileName) {
|
||||||
const lowerName = fileName.toLowerCase();
|
const lowerName = fileName.toLowerCase();
|
||||||
@@ -160,10 +160,15 @@ StyledRect {
|
|||||||
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
|
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
ClippingRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: gridPreviewImage
|
id: gridPreviewImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
|
||||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
||||||
property int weExtIndex: 0
|
property int weExtIndex: 0
|
||||||
property string imagePath: {
|
property string imagePath: {
|
||||||
@@ -175,60 +180,32 @@ StyledRect {
|
|||||||
}
|
}
|
||||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
|
if (status !== Image.Error)
|
||||||
|
return;
|
||||||
|
if (weMode && delegateRoot.fileIsDir) {
|
||||||
if (weExtIndex < weExtensions.length - 1) {
|
if (weExtIndex < weExtensions.length - 1) {
|
||||||
weExtIndex++;
|
weExtIndex++;
|
||||||
} else {
|
} else {
|
||||||
imagePath = "";
|
imagePath = "";
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (_videoThumb)
|
||||||
|
generateVideoThumbnail();
|
||||||
}
|
}
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||||
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: false
|
visible: status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
CachingImage {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
|
||||||
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
|
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
|
||||||
maxCacheSize: 256
|
maxCacheSize: 256
|
||||||
|
animate: false
|
||||||
visible: !delegateRoot.fileIsDir && isImage
|
visible: !delegateRoot.fileIsDir && isImage
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: gridImageMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
source: gridPreviewImage
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: gridImageMask
|
|
||||||
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && 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
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -95,25 +95,27 @@ StyledRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property string _videoThumb: ""
|
property string _videoThumb: ""
|
||||||
|
property bool _thumbGenAttempted: false
|
||||||
|
|
||||||
|
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||||
onVideoThumbnailPathChanged: {
|
onVideoThumbnailPathChanged: {
|
||||||
_videoThumb = "";
|
_thumbGenAttempted = false;
|
||||||
if (!videoThumbnailPath)
|
_videoThumb = videoThumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVideoThumbnail() {
|
||||||
|
if (_thumbGenAttempted)
|
||||||
return;
|
return;
|
||||||
|
_thumbGenAttempted = true;
|
||||||
|
_videoThumb = "";
|
||||||
const thumbPath = videoThumbnailPath;
|
const thumbPath = videoThumbnailPath;
|
||||||
const fp = listDelegateRoot.filePath;
|
const thumbDir = _xdgCacheHome + "/thumbnails/normal";
|
||||||
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
|
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s 128 -f";
|
||||||
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) {
|
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, listDelegateRoot.filePath, 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)
|
if (exitCode === 0)
|
||||||
_videoThumb = thumbPath;
|
_videoThumb = thumbPath;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconForFile(fileName) {
|
function getIconForFile(fileName) {
|
||||||
const lowerName = fileName.toLowerCase();
|
const lowerName = fileName.toLowerCase();
|
||||||
@@ -165,6 +167,11 @@ StyledRect {
|
|||||||
height: 28
|
height: 28
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
ClippingRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: listPreviewImage
|
id: listPreviewImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -174,45 +181,19 @@ StyledRect {
|
|||||||
sourceSize.width: 32
|
sourceSize.width: 32
|
||||||
sourceSize.height: 32
|
sourceSize.height: 32
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: false
|
visible: status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Error && _videoThumb)
|
||||||
|
generateVideoThumbnail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
CachingImage {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
|
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
|
||||||
maxCacheSize: 256
|
maxCacheSize: 256
|
||||||
|
animate: false
|
||||||
visible: !listDelegateRoot.fileIsDir && isImage
|
visible: !listDelegateRoot.fileIsDir && isImage
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: listImageMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: listPreviewImage
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: listImageMask
|
|
||||||
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: listImageMask
|
|
||||||
anchors.fill: parent
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Common
|
import qs.Modals.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -591,24 +591,11 @@ DankModal {
|
|||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||||
border.width: isSelected ? 2 : 0
|
border.width: isSelected ? 2 : 0
|
||||||
|
|
||||||
Rectangle {
|
ClippingRectangle {
|
||||||
id: gridProgressMask
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
visible: false
|
color: "transparent"
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: gridButtonRect.isHolding
|
visible: gridButtonRect.isHolding
|
||||||
layer.enabled: gridButtonRect.isHolding
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: gridProgressMask
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -729,24 +716,11 @@ DankModal {
|
|||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||||
border.width: isSelected ? 2 : 0
|
border.width: isSelected ? 2 : 0
|
||||||
|
|
||||||
Rectangle {
|
ClippingRectangle {
|
||||||
id: listProgressMask
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
visible: false
|
color: "transparent"
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: listButtonRect.isHolding
|
visible: listButtonRect.isHolding
|
||||||
layer.enabled: listButtonRect.isHolding
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: listProgressMask
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -108,16 +108,48 @@ Variants {
|
|||||||
function invalidate() {
|
function invalidate() {
|
||||||
_settleFrames = 3;
|
_settleFrames = 3;
|
||||||
backingWindow?.update();
|
backingWindow?.update();
|
||||||
|
if (!_wedgeBounced)
|
||||||
|
wedgeWatchdog.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
onRenderActiveChanged: invalidate()
|
onRenderActiveChanged: invalidate()
|
||||||
onBackingWindowChanged: 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 {
|
Connections {
|
||||||
target: root.backingWindow
|
target: root.backingWindow
|
||||||
function onFrameSwapped() {
|
function onFrameSwapped() {
|
||||||
if (root._settleFrames > 0)
|
if (root._settleFrames > 0)
|
||||||
root._settleFrames--;
|
root._settleFrames--;
|
||||||
|
root._wedgeBounced = false;
|
||||||
|
wedgeWatchdog.stop();
|
||||||
}
|
}
|
||||||
function onVisibleChanged() {
|
function onVisibleChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
@@ -142,10 +174,29 @@ Variants {
|
|||||||
function onWallpaperFillModeChanged() {
|
function onWallpaperFillModeChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
function onWallpaperBackgroundColorModeChanged() {
|
function onEffectiveWallpaperBackgroundColorChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
function onWallpaperBackgroundCustomColorChanged() {
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +221,7 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSourceChanged: {
|
onSourceChanged: {
|
||||||
|
invalidate();
|
||||||
if (!source || source.startsWith("#")) {
|
if (!source || source.startsWith("#")) {
|
||||||
setWallpaperImmediate("");
|
setWallpaperImmediate("");
|
||||||
return;
|
return;
|
||||||
@@ -190,6 +242,7 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaperImmediate(newSource) {
|
function setWallpaperImmediate(newSource) {
|
||||||
|
transitionDelayTimer.stop();
|
||||||
transitionAnimation.stop();
|
transitionAnimation.stop();
|
||||||
root.transitionProgress = 0.0;
|
root.transitionProgress = 0.0;
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
|
|||||||
@@ -7,15 +7,24 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
readonly property bool hasActiveMedia: activePlayer !== null
|
readonly property bool isPlaying: activePlayer !== null && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||||
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
readonly property bool live: visible && isPlaying
|
||||||
|
|
||||||
width: 20
|
width: 20
|
||||||
height: Theme.iconSize
|
height: Theme.iconSize
|
||||||
|
|
||||||
Loader {
|
readonly property real maxBarHeight: Theme.iconSize - 2
|
||||||
active: isPlaying
|
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
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
Ref {
|
Ref {
|
||||||
service: CavaService
|
service: CavaService
|
||||||
@@ -23,15 +32,8 @@ 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 {
|
Timer {
|
||||||
id: fallbackTimer
|
running: !CavaService.cavaAvailable && root.live
|
||||||
|
|
||||||
running: !CavaService.cavaAvailable && isPlaying
|
|
||||||
interval: 500
|
interval: 500
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
@@ -41,54 +43,32 @@ Item {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CavaService
|
target: CavaService
|
||||||
|
enabled: root.live
|
||||||
function onValuesChanged() {
|
function onValuesChanged() {
|
||||||
if (!root.isPlaying) {
|
const v = CavaService.values;
|
||||||
root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight];
|
if (v.length < 6)
|
||||||
return;
|
return;
|
||||||
}
|
const n = i => {
|
||||||
|
const x = v[i];
|
||||||
const newHeights = [];
|
return x <= 0 ? 0 : x >= 100 ? 1 : Math.sqrt(x * 0.01);
|
||||||
for (let i = 0; i < 6; i++) {
|
};
|
||||||
if (CavaService.values.length <= i) {
|
bars.bandsA = Qt.vector4d(n(0), n(1), n(2), n(3));
|
||||||
newHeights.push(root.minBarHeight);
|
bars.bandsB = Qt.vector2d(n(4), n(5));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
ShaderEffect {
|
||||||
anchors.centerIn: parent
|
id: bars
|
||||||
spacing: 1.5
|
anchors.fill: parent
|
||||||
|
|
||||||
Repeater {
|
property real widthPx: width
|
||||||
model: 6
|
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)
|
||||||
|
|
||||||
Rectangle {
|
fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/viz_bars.frag.qsb")
|
||||||
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,6 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
@@ -234,16 +233,19 @@ BasePill {
|
|||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Row {
|
||||||
id: contentRow
|
id: contentRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
visible: !root.isVerticalOrientation
|
visible: !root.isVerticalOrientation
|
||||||
|
|
||||||
|
readonly property real iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: horizontalAppIcon
|
id: horizontalAppIcon
|
||||||
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
width: contentRow.iconSize
|
||||||
Layout.preferredHeight: Layout.preferredWidth
|
height: contentRow.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showIcon && activeWindow && status === Image.Ready
|
visible: root.showIcon && activeWindow && status === Image.Ready
|
||||||
source: {
|
source: {
|
||||||
if (!activeWindow || !activeWindow.appId)
|
if (!activeWindow || !activeWindow.appId)
|
||||||
@@ -264,8 +266,10 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
id: horizontalSteamIcon
|
||||||
size: Layout.preferredWidth
|
width: contentRow.iconSize
|
||||||
|
size: contentRow.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
|
visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
|
||||||
@@ -280,9 +284,11 @@ BasePill {
|
|||||||
}
|
}
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.maximumWidth: compactMode ? 80 : 180
|
width: Math.min(implicitWidth, compactMode ? 80 : 180)
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +297,7 @@ BasePill {
|
|||||||
text: compactMode ? "" : "•"
|
text: compactMode ? "" : "•"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.outlineButton
|
color: Theme.outlineButton
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: !compactMode && appText.text && titleText.text
|
visible: !compactMode && appText.text && titleText.text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,9 +325,24 @@ BasePill {
|
|||||||
}
|
}
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth
|
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));
|
||||||
|
}
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ Item {
|
|||||||
function mapWorkspace(ws) {
|
function mapWorkspace(ws) {
|
||||||
return {
|
return {
|
||||||
"num": ws.number,
|
"num": ws.number,
|
||||||
"name": ws.name,
|
"name": stripSwayWorkspaceNumber(ws.number, ws.name),
|
||||||
"focused": ws.focused,
|
"focused": ws.focused,
|
||||||
"active": ws.active,
|
"active": ws.active,
|
||||||
"urgent": ws.urgent,
|
"urgent": ws.urgent,
|
||||||
@@ -202,6 +202,18 @@ 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
|
// Numbered workspaces first in ascending order; purely-named workspaces (sway reports num -1) after, by name
|
||||||
function swayWorkspaceOrder(a, b) {
|
function swayWorkspaceOrder(a, b) {
|
||||||
const keyA = a.num === -1 ? Number.MAX_SAFE_INTEGER : a.num;
|
const keyA = a.num === -1 ? Number.MAX_SAFE_INTEGER : a.num;
|
||||||
|
|||||||
@@ -240,8 +240,10 @@ DankPopout {
|
|||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
if (root.shouldBeVisible)
|
if (!root.shouldBeVisible)
|
||||||
|
return;
|
||||||
mainContainer.forceActiveFocus();
|
mainContainer.forceActiveFocus();
|
||||||
|
tabBar.snapIndicator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +413,7 @@ DankPopout {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentTabId === "media"
|
active: root.currentTabId === "media"
|
||||||
visible: active
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
MediaPlayerTab {
|
MediaPlayerTab {
|
||||||
targetScreen: root.screen
|
targetScreen: root.screen
|
||||||
@@ -462,7 +465,7 @@ DankPopout {
|
|||||||
DankSpinner {
|
DankSpinner {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: 40
|
size: 40
|
||||||
visible: wallpaperLoader.active && wallpaperLoader.status === Loader.Loading
|
visible: (wallpaperLoader.active && wallpaperLoader.status === Loader.Loading) || (mediaLoader.active && mediaLoader.status === Loader.Loading) || (weatherLoader.active && weatherLoader.status === Loader.Loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
@@ -470,6 +473,7 @@ DankPopout {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentTabId === "weather"
|
active: root.currentTabId === "weather"
|
||||||
visible: active
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
WeatherTab {}
|
WeatherTab {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ Item {
|
|||||||
|
|
||||||
property int __panelHoverCount: 0
|
property int __panelHoverCount: 0
|
||||||
|
|
||||||
|
readonly property bool overlayBlurActive: dropdownBlur.active
|
||||||
|
|
||||||
onDropdownTypeChanged: {
|
onDropdownTypeChanged: {
|
||||||
if (dropdownType === 0) {
|
if (dropdownType === 0) {
|
||||||
__panelHoverCount = 0;
|
__panelHoverCount = 0;
|
||||||
@@ -75,8 +77,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
|
id: dropdownBlur
|
||||||
targetWindow: root.targetWindow
|
targetWindow: root.targetWindow
|
||||||
readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0
|
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
|
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
|
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
|
blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -24,6 +25,11 @@ Item {
|
|||||||
property string section: ""
|
property string section: ""
|
||||||
property int barPosition: SettingsData.Position.Top
|
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 showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||||
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
|
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
|
||||||
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||||
@@ -70,7 +76,7 @@ Item {
|
|||||||
property bool _switchHold: false
|
property bool _switchHold: false
|
||||||
Timer {
|
Timer {
|
||||||
id: _switchHoldTimer
|
id: _switchHoldTimer
|
||||||
interval: 650
|
interval: 1500
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: _switchHold = false
|
onTriggered: _switchHold = false
|
||||||
}
|
}
|
||||||
@@ -78,7 +84,8 @@ Item {
|
|||||||
onActivePlayerChanged: {
|
onActivePlayerChanged: {
|
||||||
if (!activePlayer) {
|
if (!activePlayer) {
|
||||||
isSwitching = false;
|
isSwitching = false;
|
||||||
_switchHold = false;
|
_switchHold = true;
|
||||||
|
_switchHoldTimer.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isSwitching = true;
|
isSwitching = true;
|
||||||
@@ -117,16 +124,12 @@ Item {
|
|||||||
Connections {
|
Connections {
|
||||||
target: MprisController
|
target: MprisController
|
||||||
function onAvailablePlayersChanged() {
|
function onAvailablePlayersChanged() {
|
||||||
const count = (MprisController.availablePlayers?.length || 0);
|
if ((MprisController.availablePlayers?.length || 0) === 0)
|
||||||
if (count === 0) {
|
|
||||||
isSwitching = false;
|
isSwitching = false;
|
||||||
_switchHold = false;
|
|
||||||
} else {
|
|
||||||
_switchHold = true;
|
_switchHold = true;
|
||||||
_switchHoldTimer.restart();
|
_switchHoldTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getAudioDeviceIcon(device) {
|
function getAudioDeviceIcon(device) {
|
||||||
if (!device || !device.name)
|
if (!device || !device.name)
|
||||||
@@ -290,34 +293,72 @@ Item {
|
|||||||
Item {
|
Item {
|
||||||
id: bgContainer
|
id: bgContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: TrackArtService.resolvedArtUrl !== ""
|
|
||||||
|
readonly property string curArt: TrackArtService.resolvedArtUrl
|
||||||
|
// 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 frontArt = _showA ? layerA.art : layerB.art;
|
||||||
|
const backArt = _showA ? layerB.art : layerA.art;
|
||||||
|
if (frontArt == curArt)
|
||||||
|
return;
|
||||||
|
if (backArt == curArt) {
|
||||||
|
_showA = !_showA;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_showA)
|
||||||
|
layerB.art = curArt;
|
||||||
|
else
|
||||||
|
layerA.art = curArt;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 {
|
Image {
|
||||||
id: bgImage
|
id: layerImg
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: Math.max(parent.width, parent.height) * 1.1
|
width: Math.max(parent.width, parent.height) * 1.1
|
||||||
height: width
|
height: width
|
||||||
source: TrackArtService.resolvedArtUrl
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: true
|
cache: true
|
||||||
visible: false
|
visible: false
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status === Image.Ready)
|
if (status === Image.Ready && source != "")
|
||||||
maybeFinishSwitch();
|
layer.loaded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
id: blurredBg
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
MultiEffect {
|
MultiEffect {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: bgImage.width
|
width: layerImg.width
|
||||||
height: bgImage.height
|
height: layerImg.height
|
||||||
source: bgImage
|
source: layerImg
|
||||||
blurEnabled: true
|
blurEnabled: true
|
||||||
blurMax: 64
|
blurMax: 64
|
||||||
blur: 0.8
|
blur: 0.8
|
||||||
@@ -326,22 +367,26 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
BgBlurLayer {
|
||||||
id: bgMask
|
id: layerA
|
||||||
anchors.fill: parent
|
front: bgContainer._showA
|
||||||
radius: Theme.cornerRadius
|
onLoaded: {
|
||||||
visible: false
|
if (!bgContainer._showA) {
|
||||||
layer.enabled: true
|
bgContainer._showA = true;
|
||||||
|
root.maybeFinishSwitch();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiEffect {
|
BgBlurLayer {
|
||||||
anchors.fill: parent
|
id: layerB
|
||||||
source: blurredBg
|
front: !bgContainer._showA
|
||||||
maskEnabled: true
|
onLoaded: {
|
||||||
maskSource: bgMask
|
if (bgContainer._showA) {
|
||||||
maskThresholdMin: 0.5
|
bgContainer._showA = false;
|
||||||
maskSpreadAtMin: 1.0
|
root.maybeFinishSwitch();
|
||||||
opacity: 0.7
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -442,7 +487,8 @@ Item {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
visible: text.length > 0
|
// Reserve the line so late album metadata doesn't shift the seekbar.
|
||||||
|
height: Math.max(implicitHeight, Theme.fontSizeSmall * 1.4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,13 +577,13 @@ Item {
|
|||||||
height: 40
|
height: 40
|
||||||
radius: 20
|
radius: 20
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: shuffleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
color: shuffleArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "shuffle"
|
name: "shuffle"
|
||||||
size: 20
|
size: 20
|
||||||
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
|
color: activePlayer && activePlayer.shuffle ? root.accent : Theme.surfaceText
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -593,13 +639,13 @@ Item {
|
|||||||
height: 50
|
height: 50
|
||||||
radius: 25
|
radius: 25
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: Theme.primary
|
color: root.accent
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
size: 28
|
size: 28
|
||||||
color: Theme.background
|
color: root.onAccent
|
||||||
weight: 500
|
weight: 500
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,7 +709,7 @@ Item {
|
|||||||
height: 40
|
height: 40
|
||||||
radius: 20
|
radius: 20
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: repeatArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
color: repeatArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -680,7 +726,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
size: 20
|
size: 20
|
||||||
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
|
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -719,7 +765,7 @@ Item {
|
|||||||
radius: 20
|
radius: 20
|
||||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||||
y: 185
|
y: 185
|
||||||
color: playerSelectorArea.containsMouse || playersExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
color: playerSelectorArea.containsMouse || playersExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||||
border.color: Theme.outlineStrong
|
border.color: Theme.outlineStrong
|
||||||
border.width: 1
|
border.width: 1
|
||||||
z: 100
|
z: 100
|
||||||
@@ -786,7 +832,7 @@ Item {
|
|||||||
radius: 20
|
radius: 20
|
||||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||||
y: 130
|
y: 130
|
||||||
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||||
border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
|
border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
|
||||||
border.width: 1
|
border.width: 1
|
||||||
z: 101
|
z: 101
|
||||||
@@ -798,7 +844,7 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: getVolumeIcon()
|
name: getVolumeIcon()
|
||||||
size: 18
|
size: 18
|
||||||
color: volumeAvailable && currentVolume > 0 ? Theme.primary : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
|
color: volumeAvailable && currentVolume > 0 ? root.accent : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -849,7 +895,7 @@ Item {
|
|||||||
radius: 20
|
radius: 20
|
||||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||||
y: 240
|
y: 240
|
||||||
color: audioDevicesArea.containsMouse || devicesExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
color: audioDevicesArea.containsMouse || devicesExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||||
border.color: Theme.outlineStrong
|
border.color: Theme.outlineStrong
|
||||||
border.width: 1
|
border.width: 1
|
||||||
z: 100
|
z: 100
|
||||||
|
|||||||
@@ -154,13 +154,13 @@ Card {
|
|||||||
width: 32
|
width: 32
|
||||||
height: 32
|
height: 32
|
||||||
radius: 16
|
radius: 16
|
||||||
color: Theme.primary
|
color: MediaAccentService.accent
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
size: 16
|
size: 16
|
||||||
color: Theme.background
|
color: MediaAccentService.onAccent
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.DankDash.Overview
|
import qs.Modules.DankDash.Overview
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -17,7 +18,7 @@ Item {
|
|||||||
signal navFocusRequested
|
signal navFocusRequested
|
||||||
|
|
||||||
function handleKeyEvent(event) {
|
function handleKeyEvent(event) {
|
||||||
return calendarCard.handleKeyEvent(event);
|
return calendarLoader.item ? calendarLoader.item.handleKeyEvent(event) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -57,17 +58,27 @@ Item {
|
|||||||
height: 220
|
height: 220
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calendar - bottom middle (wider and taller)
|
// Calendar - bottom middle; deferred so the grid stays off the emerge frame.
|
||||||
CalendarOverviewCard {
|
Loader {
|
||||||
id: calendarCard
|
id: calendarLoader
|
||||||
x: parent.width * 0.2 - Theme.spacingM
|
x: parent.width * 0.2 - Theme.spacingM
|
||||||
y: 100 + Theme.spacingM
|
y: 100 + Theme.spacingM
|
||||||
width: parent.width * 0.6
|
width: parent.width * 0.6
|
||||||
height: 300
|
height: 300
|
||||||
|
asynchronous: true
|
||||||
|
sourceComponent: Component {
|
||||||
|
CalendarOverviewCard {
|
||||||
onCloseDash: root.closeDash()
|
onCloseDash: root.closeDash()
|
||||||
onNavFocusRequested: root.navFocusRequested()
|
onNavFocusRequested: root.navFocusRequested()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSpinner {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
size: 32
|
||||||
|
visible: calendarLoader.status === Loader.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Media - bottom right (narrow and taller)
|
// Media - bottom right (narrow and taller)
|
||||||
MediaOverviewCard {
|
MediaOverviewCard {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -245,17 +244,6 @@ Item {
|
|||||||
size: Theme.iconSize * 2
|
size: Theme.iconSize * 2
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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 {
|
Column {
|
||||||
@@ -820,16 +808,6 @@ Item {
|
|||||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
|
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
|
||||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
|
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
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -842,16 +820,6 @@ Item {
|
|||||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
|
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
|
||||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
|
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
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -993,6 +961,7 @@ Item {
|
|||||||
ListView {
|
ListView {
|
||||||
id: hourlyList
|
id: hourlyList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
reuseItems: true
|
||||||
orientation: ListView.Horizontal
|
orientation: ListView.Horizontal
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
clip: true
|
clip: true
|
||||||
@@ -1079,6 +1048,7 @@ Item {
|
|||||||
ListView {
|
ListView {
|
||||||
id: dailyList
|
id: dailyList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
reuseItems: true
|
||||||
orientation: ListView.Horizontal
|
orientation: ListView.Horizontal
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
clip: true
|
clip: true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -567,24 +567,11 @@ Rectangle {
|
|||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||||
border.width: isSelected ? 2 : 0
|
border.width: isSelected ? 2 : 0
|
||||||
|
|
||||||
Rectangle {
|
ClippingRectangle {
|
||||||
id: gridProgressMask
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
visible: false
|
color: "transparent"
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: gridButtonRect.isHolding
|
visible: gridButtonRect.isHolding
|
||||||
layer.enabled: gridButtonRect.isHolding
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: gridProgressMask
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -700,24 +687,11 @@ Rectangle {
|
|||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||||
border.width: isSelected ? 2 : 0
|
border.width: isSelected ? 2 : 0
|
||||||
|
|
||||||
Rectangle {
|
ClippingRectangle {
|
||||||
id: listProgressMask
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
visible: false
|
color: "transparent"
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: listButtonRect.isHolding
|
visible: listButtonRect.isHolding
|
||||||
layer.enabled: listButtonRect.isHolding
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: listProgressMask
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -13,12 +13,19 @@ Column {
|
|||||||
property int draggedIndex: -1
|
property int draggedIndex: -1
|
||||||
property int dropTargetIndex: -1
|
property int dropTargetIndex: -1
|
||||||
property bool suppressShiftAnimation: false
|
property bool suppressShiftAnimation: false
|
||||||
readonly property real tabItemSize: 128 + Theme.spacingXS
|
property int editingIndex: -1
|
||||||
|
readonly property real tabItemSize: tabRow.dynamicTabWidth + Theme.spacingXS
|
||||||
|
|
||||||
signal tabSwitched(int tabIndex)
|
signal tabSwitched(int tabIndex)
|
||||||
signal tabClosed(int tabIndex)
|
signal tabClosed(int tabIndex)
|
||||||
signal newTabRequested
|
signal newTabRequested
|
||||||
|
|
||||||
|
function commitRename(index, newTitle) {
|
||||||
|
if (index >= 0)
|
||||||
|
NotepadStorageService.renameTab(index, newTitle);
|
||||||
|
editingIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
function hasUnsavedChangesForTab(tab) {
|
function hasUnsavedChangesForTab(tab) {
|
||||||
if (!tab)
|
if (!tab)
|
||||||
return false;
|
return false;
|
||||||
@@ -32,11 +39,19 @@ Column {
|
|||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
id: tabRow
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 36
|
height: 36
|
||||||
spacing: Theme.spacingXS
|
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 {
|
ScrollView {
|
||||||
|
id: tabScroll
|
||||||
width: parent.width - newTabButton.width - Theme.spacingXS
|
width: parent.width - newTabButton.width - Theme.spacingXS
|
||||||
height: parent.height
|
height: parent.height
|
||||||
clip: true
|
clip: true
|
||||||
@@ -57,7 +72,8 @@ Column {
|
|||||||
|
|
||||||
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
|
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
|
||||||
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
|
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
|
||||||
readonly property real tabWidth: 128
|
readonly property bool editing: root.editingIndex === index
|
||||||
|
readonly property real tabWidth: tabRow.dynamicTabWidth
|
||||||
property bool longPressing: false
|
property bool longPressing: false
|
||||||
property bool dragging: false
|
property bool dragging: false
|
||||||
property point dragStartPos: Qt.point(0, 0)
|
property point dragStartPos: Qt.point(0, 0)
|
||||||
@@ -138,6 +154,7 @@ Column {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: tabText
|
id: tabText
|
||||||
|
visible: !delegateItem.editing
|
||||||
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
|
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
|
||||||
text: {
|
text: {
|
||||||
var prefix = "";
|
var prefix = "";
|
||||||
@@ -155,13 +172,54 @@ Column {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
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 {
|
Rectangle {
|
||||||
id: tabCloseButton
|
id: tabCloseButton
|
||||||
width: 20
|
width: 20
|
||||||
height: 20
|
height: 20
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0)
|
color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0)
|
||||||
visible: NotepadStorageService.tabs.length > 1
|
visible: NotepadStorageService.tabs.length > 1 && !delegateItem.editing
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -195,11 +253,24 @@ Column {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
id: tabMouseArea
|
id: tabMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
enabled: !delegateItem.editing
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: dragging || longPressing
|
preventStealing: dragging || longPressing
|
||||||
cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
||||||
acceptedButtons: Qt.LeftButton
|
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 => {
|
onPressed: mouse => {
|
||||||
if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) {
|
if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) {
|
||||||
delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y);
|
delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||||
@@ -279,4 +350,8 @@ Column {
|
|||||||
onClicked: root.newTabRequested()
|
onClicked: root.newTabRequested()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankTooltipV2 {
|
||||||
|
id: tabTooltip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import qs.Common
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
|
import Quickshell.Widgets
|
||||||
|
|
||||||
DankOSD {
|
DankOSD {
|
||||||
id: root
|
id: root
|
||||||
@@ -185,10 +186,11 @@ DankOSD {
|
|||||||
visible: false
|
visible: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
ClippingRectangle {
|
||||||
id: blurredBg
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: false
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
opacity: 0.7
|
||||||
|
|
||||||
MultiEffect {
|
MultiEffect {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -203,24 +205,6 @@ 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 {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import QtCore
|
import QtCore
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.FileBrowser
|
import qs.Modals.FileBrowser
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -520,9 +520,14 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
Image {
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
radius: Theme.cornerRadius - 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
var wp = Theme.wallpaperPath;
|
var wp = Theme.wallpaperPath;
|
||||||
if (!wp || wp === "" || wp.startsWith("#"))
|
if (!wp || wp === "" || wp.startsWith("#"))
|
||||||
@@ -536,12 +541,6 @@ Item {
|
|||||||
sourceSize.width: 120
|
sourceSize.width: 120
|
||||||
sourceSize.height: 120
|
sourceSize.height: 120
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: autoWallpaperMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,16 +552,6 @@ Item {
|
|||||||
visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#")
|
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 {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
|
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.FileBrowser
|
import qs.Modals.FileBrowser
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -75,9 +75,14 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
Image {
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
radius: Theme.cornerRadius - 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
var wp = root.currentWallpaper;
|
var wp = root.currentWallpaper;
|
||||||
if (wp === "" || wp.startsWith("#"))
|
if (wp === "" || wp.startsWith("#"))
|
||||||
@@ -91,12 +96,6 @@ Item {
|
|||||||
sourceSize.width: 160
|
sourceSize.width: 160
|
||||||
sourceSize.height: 160
|
sourceSize.height: 160
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: wallpaperMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,16 +107,6 @@ Item {
|
|||||||
visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#")
|
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 {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "image"
|
name: "image"
|
||||||
@@ -421,9 +410,14 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
Image {
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
radius: Theme.cornerRadius - 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
var wp = SessionData.wallpaperPathLight;
|
var wp = SessionData.wallpaperPathLight;
|
||||||
if (wp === "" || wp.startsWith("#"))
|
if (wp === "" || wp.startsWith("#"))
|
||||||
@@ -440,12 +434,6 @@ Item {
|
|||||||
sourceSize.width: 160
|
sourceSize.width: 160
|
||||||
sourceSize.height: 160
|
sourceSize.height: 160
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: lightMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,16 +451,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: lightMask
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 1
|
|
||||||
radius: Theme.cornerRadius - 1
|
|
||||||
color: "black"
|
|
||||||
visible: false
|
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "light_mode"
|
name: "light_mode"
|
||||||
@@ -611,9 +589,14 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
Image {
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
radius: Theme.cornerRadius - 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
var wp = SessionData.wallpaperPathDark;
|
var wp = SessionData.wallpaperPathDark;
|
||||||
if (wp === "" || wp.startsWith("#"))
|
if (wp === "" || wp.startsWith("#"))
|
||||||
@@ -630,12 +613,6 @@ Item {
|
|||||||
sourceSize.width: 160
|
sourceSize.width: 160
|
||||||
sourceSize.height: 160
|
sourceSize.height: 160
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: darkMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,16 +630,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: darkMask
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 1
|
|
||||||
radius: Theme.cornerRadius - 1
|
|
||||||
color: "black"
|
|
||||||
visible: false
|
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "dark_mode"
|
name: "dark_mode"
|
||||||
|
|||||||
@@ -141,16 +141,37 @@ Variants {
|
|||||||
function invalidate() {
|
function invalidate() {
|
||||||
_settleFrames = 3;
|
_settleFrames = 3;
|
||||||
backingWindow?.update();
|
backingWindow?.update();
|
||||||
|
if (!_wedgeBounced)
|
||||||
|
wedgeWatchdog.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
onRenderActiveChanged: invalidate()
|
onRenderActiveChanged: invalidate()
|
||||||
onBackingWindowChanged: 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 {
|
Connections {
|
||||||
target: root.backingWindow
|
target: root.backingWindow
|
||||||
function onFrameSwapped() {
|
function onFrameSwapped() {
|
||||||
if (root._settleFrames > 0)
|
if (root._settleFrames > 0)
|
||||||
root._settleFrames--;
|
root._settleFrames--;
|
||||||
|
root._wedgeBounced = false;
|
||||||
|
wedgeWatchdog.stop();
|
||||||
}
|
}
|
||||||
function onVisibleChanged() {
|
function onVisibleChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
@@ -176,10 +197,29 @@ Variants {
|
|||||||
function onWallpaperFillModeChanged() {
|
function onWallpaperFillModeChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
function onWallpaperBackgroundColorModeChanged() {
|
function onEffectiveWallpaperBackgroundColorChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
function onWallpaperBackgroundCustomColorChanged() {
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,13 +542,13 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSourceChanged: {
|
onSourceChanged: {
|
||||||
|
invalidate();
|
||||||
if (!source || source.startsWith("#")) {
|
if (!source || source.startsWith("#")) {
|
||||||
setWallpaperImmediate("");
|
setWallpaperImmediate("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
root.changePending = true;
|
root.changePending = true;
|
||||||
invalidate();
|
|
||||||
|
|
||||||
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
||||||
|
|
||||||
@@ -528,10 +568,14 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaperImmediate(newSource) {
|
function setWallpaperImmediate(newSource) {
|
||||||
|
transitionDelayTimer.stop();
|
||||||
transitionAnimation.stop();
|
transitionAnimation.stop();
|
||||||
root.transitionProgress = 0.0;
|
root.transitionProgress = 0.0;
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
root.screenScale = CompositorService.getScreenScale(modelData);
|
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;
|
currentWallpaper.source = newSource;
|
||||||
nextWallpaper.source = "";
|
nextWallpaper.source = "";
|
||||||
|
|
||||||
@@ -567,10 +611,14 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeWallpaper(newPath, force) {
|
function changeWallpaper(newPath, force) {
|
||||||
if (!force && newPath === currentWallpaper.source)
|
if (!force && newPath === currentWallpaper.source.toString()) {
|
||||||
|
root.changePending = false;
|
||||||
return;
|
return;
|
||||||
if (!newPath || newPath.startsWith("#"))
|
}
|
||||||
|
if (!newPath || newPath.startsWith("#")) {
|
||||||
|
root.changePending = false;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
root.screenScale = CompositorService.getScreenScale(modelData);
|
root.screenScale = CompositorService.getScreenScale(modelData);
|
||||||
if (root.transitioning || root.effectActive) {
|
if (root.transitioning || root.effectActive) {
|
||||||
root.pendingWallpaper = newPath;
|
root.pendingWallpaper = newPath;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -58,23 +58,6 @@ Item {
|
|||||||
visible: intersectsViewport
|
visible: intersectsViewport
|
||||||
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
|
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 {
|
Behavior on x {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||||
@@ -104,6 +87,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClippingRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
ScreencopyView {
|
ScreencopyView {
|
||||||
id: windowPreview
|
id: windowPreview
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -113,9 +101,7 @@ Item {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) :
|
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) : Theme.withAlpha(Theme.surfaceContainer, 0.1)
|
||||||
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
|
|
||||||
Theme.withAlpha(Theme.surfaceContainer, 0.1)
|
|
||||||
border.color: Theme.withAlpha(Theme.outline, 0.3)
|
border.color: Theme.withAlpha(Theme.outline, 0.3)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
}
|
}
|
||||||
@@ -129,7 +115,7 @@ Item {
|
|||||||
Image {
|
Image {
|
||||||
id: windowIcon
|
id: windowIcon
|
||||||
property var iconSize: {
|
property var iconSize: {
|
||||||
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1)
|
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
|
||||||
}
|
}
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
source: root.iconPath
|
source: root.iconPath
|
||||||
@@ -154,4 +140,5 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,7 +298,11 @@ Singleton {
|
|||||||
function getBuiltInLauncherItems(pluginId, query) {
|
function getBuiltInLauncherItems(pluginId, query) {
|
||||||
if (pluginId === "dms_clipboard_search") {
|
if (pluginId === "dms_clipboard_search") {
|
||||||
const trimmed = (query || "").toString().trim();
|
const trimmed = (query || "").toString().trim();
|
||||||
const entries = ClipboardService.internalEntries.length > 0 ? ClipboardService.getLauncherEntries(trimmed, 20, 0) : ClipboardService.getCachedLauncherSearchEntries(trimmed, 20);
|
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);
|
||||||
|
});
|
||||||
return entries.map(entry => ({
|
return entries.map(entry => ({
|
||||||
type: "clipboard",
|
type: "clipboard",
|
||||||
data: entry
|
data: entry
|
||||||
|
|||||||
@@ -841,13 +841,32 @@ EOFCONFIG
|
|||||||
function setVolume(percentage) {
|
function setVolume(percentage) {
|
||||||
if (!root.sink?.audio)
|
if (!root.sink?.audio)
|
||||||
return "No audio sink available";
|
return "No audio sink available";
|
||||||
|
if (isNaN(percentage))
|
||||||
|
return "Invalid percentage";
|
||||||
|
|
||||||
const maxVol = root.sinkMaxVolume;
|
const maxVol = root.sinkMaxVolume;
|
||||||
const clampedVolume = Math.max(0, Math.min(maxVol, percentage));
|
const clampedVolume = Math.max(0, Math.min(maxVol, percentage));
|
||||||
root.sink.audio.volume = clampedVolume / 100;
|
Quickshell.execDetached(["wpctl", "set-volume", "-l", String(maxVol / 100), "@DEFAULT_AUDIO_SINK@", String(clampedVolume / 100)]);
|
||||||
return `Volume set to ${clampedVolume}%`;
|
return `Volume set to ${clampedVolume}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function outputVolumeStep(step) {
|
||||||
|
const parsed = parseInt(step || "5");
|
||||||
|
return isNaN(parsed) ? 5 : Math.max(0, parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustDefaultSinkVolume(step, direction) {
|
||||||
|
const maxVol = root.sinkMaxVolume;
|
||||||
|
const stepValue = outputVolumeStep(step);
|
||||||
|
const currentVolume = Math.round(root.sink.audio.volume * 100);
|
||||||
|
const newVolume = Math.max(0, Math.min(maxVol, currentVolume + direction * stepValue));
|
||||||
|
const suffix = direction > 0 ? "+" : "-";
|
||||||
|
|
||||||
|
Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "0"]);
|
||||||
|
Quickshell.execDetached(["wpctl", "set-volume", "-l", String(maxVol / 100), "@DEFAULT_AUDIO_SINK@", `${stepValue}%${suffix}`]);
|
||||||
|
return newVolume;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleMute() {
|
function toggleMute() {
|
||||||
if (!root.sink?.audio) {
|
if (!root.sink?.audio) {
|
||||||
return "No audio sink available";
|
return "No audio sink available";
|
||||||
@@ -939,15 +958,7 @@ EOFCONFIG
|
|||||||
if (!root.sink?.audio)
|
if (!root.sink?.audio)
|
||||||
return "No audio sink available";
|
return "No audio sink available";
|
||||||
|
|
||||||
if (root.sink.audio.muted)
|
const newVolume = root.adjustDefaultSinkVolume(step, 1);
|
||||||
root.sink.audio.muted = false;
|
|
||||||
|
|
||||||
const maxVol = root.sinkMaxVolume;
|
|
||||||
const currentVolume = Math.round(root.sink.audio.volume * 100);
|
|
||||||
const stepValue = parseInt(step || "5");
|
|
||||||
const newVolume = Math.max(0, Math.min(maxVol, currentVolume + stepValue));
|
|
||||||
|
|
||||||
root.sink.audio.volume = newVolume / 100;
|
|
||||||
return `Volume increased to ${newVolume}%`;
|
return `Volume increased to ${newVolume}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,15 +966,7 @@ EOFCONFIG
|
|||||||
if (!root.sink?.audio)
|
if (!root.sink?.audio)
|
||||||
return "No audio sink available";
|
return "No audio sink available";
|
||||||
|
|
||||||
if (root.sink.audio.muted)
|
const newVolume = root.adjustDefaultSinkVolume(step, -1);
|
||||||
root.sink.audio.muted = false;
|
|
||||||
|
|
||||||
const maxVol = root.sinkMaxVolume;
|
|
||||||
const currentVolume = Math.round(root.sink.audio.volume * 100);
|
|
||||||
const stepValue = parseInt(step || "5");
|
|
||||||
const newVolume = Math.max(0, Math.min(maxVol, currentVolume - stepValue));
|
|
||||||
|
|
||||||
root.sink.audio.volume = newVolume / 100;
|
|
||||||
return `Volume decreased to ${newVolume}%`;
|
return `Volume decreased to ${newVolume}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("ChangelogService")
|
readonly property var log: Log.scoped("ChangelogService")
|
||||||
|
|
||||||
readonly property string currentVersion: "1.4"
|
readonly property string currentVersion: "1.5"
|
||||||
readonly property bool changelogEnabled: false
|
readonly property bool changelogEnabled: true
|
||||||
|
|
||||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
|
||||||
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
|
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ Singleton {
|
|||||||
property int selectedIndex: 0
|
property int selectedIndex: 0
|
||||||
property bool keyboardNavigationActive: false
|
property bool keyboardNavigationActive: false
|
||||||
property int refCount: 0
|
property int refCount: 0
|
||||||
property real _launcherLastRefresh: 0
|
|
||||||
property bool _launcherCacheValid: false
|
property bool _launcherCacheValid: false
|
||||||
property string _launcherCachedQuery: ""
|
property string _launcherCachedQuery: ""
|
||||||
property var _launcherCachedEntries: []
|
property var _launcherCachedEntries: []
|
||||||
@@ -85,31 +84,30 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
let filtered = internalEntries;
|
const query = searchText.trim().toLowerCase();
|
||||||
|
const filterAll = activeFilter === "all";
|
||||||
|
const unpinned = [];
|
||||||
|
const pinned = [];
|
||||||
|
|
||||||
if (activeFilter !== "all") {
|
for (let i = 0; i < internalEntries.length; i++) {
|
||||||
filtered = filtered.filter(entry => getEntryType(entry) === activeFilter);
|
const entry = internalEntries[i];
|
||||||
|
if (!filterAll && getEntryType(entry) !== activeFilter)
|
||||||
|
continue;
|
||||||
|
if (query.length > 0 && !entry.preview.toLowerCase().includes(query))
|
||||||
|
continue;
|
||||||
|
(entry.pinned ? pinned : unpinned).push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = searchText.trim();
|
const byIdDesc = (a, b) => b.id - a.id;
|
||||||
|
pinned.sort(byIdDesc);
|
||||||
|
unpinned.sort(byIdDesc);
|
||||||
|
|
||||||
if (query.length > 0) {
|
pinnedEntries = pinned;
|
||||||
const lowerQuery = query.toLowerCase();
|
unpinnedEntries = unpinned;
|
||||||
filtered = filtered.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
|
clipboardEntries = pinned.concat(unpinned);
|
||||||
}
|
|
||||||
|
|
||||||
filtered.sort((a, b) => {
|
|
||||||
if (a.pinned !== b.pinned)
|
|
||||||
return b.pinned ? 1 : -1;
|
|
||||||
return b.id - a.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
clipboardEntries = filtered;
|
|
||||||
unpinnedEntries = filtered.filter(e => !e.pinned);
|
|
||||||
pinnedEntries = filtered.filter(e => e.pinned);
|
|
||||||
totalCount = clipboardEntries.length;
|
totalCount = clipboardEntries.length;
|
||||||
|
|
||||||
const activeCount = Math.max(unpinnedEntries.length, pinnedEntries.length);
|
const activeCount = Math.max(unpinned.length, pinned.length);
|
||||||
|
|
||||||
if (activeCount === 0) {
|
if (activeCount === 0) {
|
||||||
keyboardNavigationActive = false;
|
keyboardNavigationActive = false;
|
||||||
@@ -117,10 +115,9 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedIndex >= activeCount) {
|
if (selectedIndex >= activeCount)
|
||||||
selectedIndex = activeCount - 1;
|
selectedIndex = activeCount - 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
if (!clipboardAvailable) {
|
if (!clipboardAvailable) {
|
||||||
@@ -138,18 +135,6 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureLauncherHistory() {
|
|
||||||
if (!clipboardAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (internalEntries.length === 0 || now - _launcherLastRefresh > 5000) {
|
|
||||||
_launcherLastRefresh = now;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestLauncherSearch(query, limit) {
|
function requestLauncherSearch(query, limit) {
|
||||||
if (!clipboardAvailable) {
|
if (!clipboardAvailable) {
|
||||||
return;
|
return;
|
||||||
@@ -207,56 +192,6 @@ Singleton {
|
|||||||
_launcherSearchSeq++;
|
_launcherSearchSeq++;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLauncherEntries(query, limit, minLength) {
|
|
||||||
if (!clipboardAvailable) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = (query || "").toString().trim();
|
|
||||||
const requiredLength = minLength !== undefined ? minLength : 2;
|
|
||||||
if (trimmed.length < requiredLength) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowerQuery = trimmed.toLowerCase();
|
|
||||||
const maxItems = limit > 0 ? limit : 8;
|
|
||||||
const matches = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < internalEntries.length; i++) {
|
|
||||||
const entry = internalEntries[i];
|
|
||||||
const preview = getEntryPreview(entry).toString();
|
|
||||||
const typeText = entry.isImage ? "image picture screenshot clipboard" : "text clipboard";
|
|
||||||
const haystack = (preview + " " + typeText).toLowerCase();
|
|
||||||
if (haystack.indexOf(lowerQuery) === -1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
matches.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
matches.sort((a, b) => {
|
|
||||||
if (a.pinned !== b.pinned)
|
|
||||||
return b.pinned ? 1 : -1;
|
|
||||||
return (b.id || 0) - (a.id || 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
return matches.slice(0, maxItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecentLauncherEntries(limit) {
|
|
||||||
if (!clipboardAvailable) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxItems = limit > 0 ? limit : 20;
|
|
||||||
const entries = internalEntries.slice();
|
|
||||||
entries.sort((a, b) => {
|
|
||||||
if (a.pinned !== b.pinned)
|
|
||||||
return b.pinned ? 1 : -1;
|
|
||||||
return (b.id || 0) - (a.id || 0);
|
|
||||||
});
|
|
||||||
return entries.slice(0, maxItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
searchText = "";
|
searchText = "";
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
// Accent color extracted from the current track's album art via ColorQuantizer,
|
||||||
|
// falling back to Theme.primary when no usable accent is available.
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool hasAccent: _accent !== null
|
||||||
|
readonly property color accent: _accent !== null ? _accent : Theme.primary
|
||||||
|
|
||||||
|
readonly property color onAccent: {
|
||||||
|
const c = accent;
|
||||||
|
const lum = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
|
||||||
|
return lum > 0.6 ? Qt.rgba(0, 0, 0, 1) : Qt.rgba(1, 1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color accentHover: Theme.withAlpha(accent, 0.12)
|
||||||
|
readonly property color accentPressed: Theme.withAlpha(accent, Theme.transparentBlurLayers ? 0.24 : 0.16)
|
||||||
|
|
||||||
|
readonly property color accentTrack: Theme.withAlpha(accent, 0.28)
|
||||||
|
readonly property color accentSubtle: Theme.withAlpha(accent, 0.55)
|
||||||
|
|
||||||
|
// Plain-named alias: underscore-prefixed props with onChanged handlers crash config load.
|
||||||
|
readonly property string artUrl: TrackArtService.resolvedArtUrl
|
||||||
|
onArtUrlChanged: {
|
||||||
|
if (artUrl === "")
|
||||||
|
_accent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
property var _accent: null
|
||||||
|
|
||||||
|
ColorQuantizer {
|
||||||
|
id: quantizer
|
||||||
|
source: root.artUrl
|
||||||
|
depth: 4
|
||||||
|
rescaleSize: 64
|
||||||
|
onColorsChanged: root._accent = root._pickAccent(colors)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pickAccent(colors) {
|
||||||
|
if (!colors || colors.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
let best = null;
|
||||||
|
let bestScore = -1;
|
||||||
|
for (let i = 0; i < colors.length; i++) {
|
||||||
|
const c = colors[i];
|
||||||
|
const s = c.hsvSaturation;
|
||||||
|
const v = c.hsvValue;
|
||||||
|
if (v < 0.22 || v > 0.96 || s < 0.22)
|
||||||
|
continue;
|
||||||
|
const score = s * (1 - Math.abs(v - 0.68));
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
best = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!best)
|
||||||
|
return null;
|
||||||
|
return _normalize(best);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _normalize(c) {
|
||||||
|
const hue = c.hsvHue < 0 ? 0 : c.hsvHue;
|
||||||
|
const s = Math.min(1, c.hsvSaturation * 1.05);
|
||||||
|
const v = Math.max(c.hsvValue, 0.62);
|
||||||
|
return Qt.hsva(hue, s, v, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,45 +29,48 @@ Singleton {
|
|||||||
|
|
||||||
function setSessionBuffer(tabId, content, baseline) {
|
function setSessionBuffer(tabId, content, baseline) {
|
||||||
if (tabId === undefined || tabId === null || tabId < 0)
|
if (tabId === undefined || tabId === null || tabId < 0)
|
||||||
return
|
return;
|
||||||
var next = Object.assign({}, sessionBuffers)
|
var next = Object.assign({}, sessionBuffers);
|
||||||
if (content !== baseline) {
|
if (content !== baseline) {
|
||||||
next[tabId] = { content: content, baseline: baseline }
|
next[tabId] = {
|
||||||
|
content: content,
|
||||||
|
baseline: baseline
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
delete next[tabId]
|
delete next[tabId];
|
||||||
}
|
}
|
||||||
sessionBuffers = next
|
sessionBuffers = next;
|
||||||
sessionBufferRevision++
|
sessionBufferRevision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSessionBuffer(tabId) {
|
function getSessionBuffer(tabId) {
|
||||||
return sessionBuffers[tabId]
|
return sessionBuffers[tabId];
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSessionBuffer(tabId) {
|
function clearSessionBuffer(tabId) {
|
||||||
if (sessionBuffers[tabId] === undefined)
|
if (sessionBuffers[tabId] === undefined)
|
||||||
return
|
return;
|
||||||
var next = Object.assign({}, sessionBuffers)
|
var next = Object.assign({}, sessionBuffers);
|
||||||
delete next[tabId]
|
delete next[tabId];
|
||||||
sessionBuffers = next
|
sessionBuffers = next;
|
||||||
sessionBufferRevision++
|
sessionBufferRevision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
property var conflictTabId: -1
|
property var conflictTabId: -1
|
||||||
property string conflictDiskContent: ""
|
property string conflictDiskContent: ""
|
||||||
|
|
||||||
function flagConflict(tabId, diskContent) {
|
function flagConflict(tabId, diskContent) {
|
||||||
conflictDiskContent = diskContent
|
conflictDiskContent = diskContent;
|
||||||
conflictTabId = tabId
|
conflictTabId = tabId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearConflict() {
|
function clearConflict() {
|
||||||
conflictTabId = -1
|
conflictTabId = -1;
|
||||||
conflictDiskContent = ""
|
conflictDiskContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
ensureDirectories()
|
ensureDirectories();
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
@@ -78,49 +81,50 @@ Singleton {
|
|||||||
|
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
try {
|
try {
|
||||||
var data = JSON.parse(text())
|
var data = JSON.parse(text());
|
||||||
root.tabs = data.tabs || []
|
root.tabs = data.tabs || [];
|
||||||
root.currentTabIndex = data.currentTabIndex || 0
|
root.currentTabIndex = data.currentTabIndex || 0;
|
||||||
root.metadataLoaded = true
|
root.metadataLoaded = true;
|
||||||
root.validateTabs()
|
root.validateTabs();
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
log.warn("Failed to parse notepad metadata:", e)
|
log.warn("Failed to parse notepad metadata:", e);
|
||||||
root.createDefaultTab()
|
root.createDefaultTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoadFailed: {
|
onLoadFailed: {
|
||||||
root.createDefaultTab()
|
root.createDefaultTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRefCountChanged: {
|
onRefCountChanged: {
|
||||||
if (refCount === 1 && !metadataLoaded) {
|
if (refCount === 1 && !metadataLoaded) {
|
||||||
metadataFile.path = ""
|
metadataFile.path = "";
|
||||||
metadataFile.path = root.metadataPath
|
metadataFile.path = root.metadataPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDirectories() {
|
function ensureDirectories() {
|
||||||
mkdirProcess.running = true
|
Proc.runCommand("", ["mkdir", "-p", root.baseDir, root.filesDir], null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMetadata() {
|
function loadMetadata() {
|
||||||
metadataFile.path = ""
|
metadataFile.path = "";
|
||||||
metadataFile.path = root.metadataPath
|
metadataFile.path = root.metadataPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultTab() {
|
function createDefaultTab() {
|
||||||
var id = Date.now()
|
var id = Date.now();
|
||||||
var filePath = "notepad-files/untitled-" + id + ".txt"
|
var filePath = "notepad-files/untitled-" + id + ".txt";
|
||||||
var fullPath = baseDir + "/" + filePath
|
var fullPath = baseDir + "/" + filePath;
|
||||||
|
|
||||||
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
newTabsBeingCreated[id] = true
|
newTabsBeingCreated[id] = true;
|
||||||
tabsBeingCreated = newTabsBeingCreated
|
tabsBeingCreated = newTabsBeingCreated;
|
||||||
|
|
||||||
root.createEmptyFile(fullPath, function() {
|
root.createEmptyFile(fullPath, function () {
|
||||||
root.tabs = [{
|
root.tabs = [
|
||||||
|
{
|
||||||
id: id,
|
id: id,
|
||||||
title: I18n.tr("Untitled"),
|
title: I18n.tr("Untitled"),
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
@@ -128,14 +132,15 @@ Singleton {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
cursorPosition: 0,
|
cursorPosition: 0,
|
||||||
scrollPosition: 0
|
scrollPosition: 0
|
||||||
}]
|
}
|
||||||
root.currentTabIndex = 0
|
];
|
||||||
|
root.currentTabIndex = 0;
|
||||||
|
|
||||||
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
delete updatedTabsBeingCreated[id]
|
delete updatedTabsBeingCreated[id];
|
||||||
tabsBeingCreated = updatedTabsBeingCreated
|
tabsBeingCreated = updatedTabsBeingCreated;
|
||||||
root.saveMetadata()
|
root.saveMetadata();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMetadata() {
|
function saveMetadata() {
|
||||||
@@ -143,82 +148,73 @@ Singleton {
|
|||||||
version: 1,
|
version: 1,
|
||||||
currentTabIndex: currentTabIndex,
|
currentTabIndex: currentTabIndex,
|
||||||
tabs: tabs
|
tabs: tabs
|
||||||
}
|
};
|
||||||
metadataFile.setText(JSON.stringify(metadata, null, 2))
|
metadataFile.setText(JSON.stringify(metadata, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabById(tabId) {
|
function getTabById(tabId) {
|
||||||
for (var i = 0; i < tabs.length; i++) {
|
for (var i = 0; i < tabs.length; i++) {
|
||||||
if (tabs[i].id === tabId)
|
if (tabs[i].id === tabId)
|
||||||
return tabs[i]
|
return tabs[i];
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTabContent(tabIndex, callback) {
|
function loadTabContent(tabIndex, callback) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) {
|
if (tabIndex < 0 || tabIndex >= tabs.length) {
|
||||||
callback("")
|
callback("");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tab = tabs[tabIndex]
|
var tab = tabs[tabIndex];
|
||||||
var requestTabId = tab.id
|
var requestTabId = tab.id;
|
||||||
var fullPath = tab.isTemporary
|
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath;
|
||||||
? baseDir + "/" + tab.filePath
|
|
||||||
: tab.filePath
|
|
||||||
|
|
||||||
if (tabsBeingCreated[tab.id]) {
|
if (tabsBeingCreated[tab.id]) {
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
loadTabContent(tabIndex, callback)
|
loadTabContent(tabIndex, callback);
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileChecker = fileExistsComponent.createObject(root, {
|
Proc.runCommand("", ["test", "-f", fullPath], (output, exitCode) => {
|
||||||
path: fullPath,
|
var currentTab = root.getTabById(requestTabId);
|
||||||
callback: (exists) => {
|
var currentPath = currentTab ? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath) : "";
|
||||||
var currentTab = root.getTabById(requestTabId)
|
|
||||||
var currentPath = currentTab
|
|
||||||
? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath)
|
|
||||||
: ""
|
|
||||||
|
|
||||||
if (!currentTab || currentPath !== fullPath) {
|
if (!currentTab || currentPath !== fullPath) {
|
||||||
callback("")
|
callback("");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exists) {
|
if (exitCode === 0) {
|
||||||
var loader = tabFileLoaderComponent.createObject(root, {
|
tabFileLoaderComponent.createObject(root, {
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
callback: callback
|
callback: callback
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
log.warn("Tab file does not exist:", fullPath)
|
log.warn("Tab file does not exist:", fullPath);
|
||||||
callback("")
|
callback("");
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTabContent(tabIndex, content) {
|
function saveTabContent(tabIndex, content) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
var tab = tabs[tabIndex]
|
var tab = tabs[tabIndex];
|
||||||
var fullPath = tab.isTemporary
|
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath;
|
||||||
? baseDir + "/" + tab.filePath
|
|
||||||
: tab.filePath
|
|
||||||
|
|
||||||
var saver = tabFileSaverComponent.createObject(root, {
|
var saver = tabFileSaverComponent.createObject(root, {
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
content: content,
|
content: content,
|
||||||
tabIndex: tabIndex
|
tabIndex: tabIndex
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNewTab() {
|
function createNewTab() {
|
||||||
var id = Date.now()
|
var id = Date.now();
|
||||||
var filePath = "notepad-files/untitled-" + id + ".txt"
|
var filePath = "notepad-files/untitled-" + id + ".txt";
|
||||||
var fullPath = baseDir + "/" + filePath
|
var fullPath = baseDir + "/" + filePath;
|
||||||
|
|
||||||
var newTab = {
|
var newTab = {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -228,29 +224,29 @@ Singleton {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
cursorPosition: 0,
|
cursorPosition: 0,
|
||||||
scrollPosition: 0
|
scrollPosition: 0
|
||||||
}
|
};
|
||||||
|
|
||||||
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
newTabsBeingCreated[id] = true
|
newTabsBeingCreated[id] = true;
|
||||||
tabsBeingCreated = newTabsBeingCreated
|
tabsBeingCreated = newTabsBeingCreated;
|
||||||
createEmptyFile(fullPath, function() {
|
createEmptyFile(fullPath, function () {
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
newTabs.push(newTab)
|
newTabs.push(newTab);
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
currentTabIndex = tabs.length - 1
|
currentTabIndex = tabs.length - 1;
|
||||||
|
|
||||||
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
delete updatedTabsBeingCreated[id]
|
delete updatedTabsBeingCreated[id];
|
||||||
tabsBeingCreated = updatedTabsBeingCreated
|
tabsBeingCreated = updatedTabsBeingCreated;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
})
|
});
|
||||||
|
|
||||||
return newTab
|
return newTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTabForFile(path) {
|
function createTabForFile(path) {
|
||||||
var id = Date.now()
|
var id = Date.now();
|
||||||
var fileName = path.split('/').pop()
|
var fileName = path.split('/').pop();
|
||||||
|
|
||||||
var newTab = {
|
var newTab = {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -260,34 +256,34 @@ Singleton {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
cursorPosition: 0,
|
cursorPosition: 0,
|
||||||
scrollPosition: 0
|
scrollPosition: 0
|
||||||
}
|
};
|
||||||
|
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
newTabs.push(newTab)
|
newTabs.push(newTab);
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
currentTabIndex = tabs.length - 1
|
currentTabIndex = tabs.length - 1;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
|
|
||||||
return newTab
|
return newTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTab(tabIndex) {
|
function closeTab(tabIndex) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1
|
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1;
|
||||||
clearSessionBuffer(closedTabId)
|
clearSessionBuffer(closedTabId);
|
||||||
if (conflictTabId === closedTabId)
|
if (conflictTabId === closedTabId)
|
||||||
clearConflict()
|
clearConflict();
|
||||||
|
|
||||||
if (newTabs.length <= 1) {
|
if (newTabs.length <= 1) {
|
||||||
var id = Date.now()
|
var id = Date.now();
|
||||||
var filePath = "notepad-files/untitled-" + id + ".txt"
|
var filePath = "notepad-files/untitled-" + id + ".txt";
|
||||||
|
|
||||||
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
newTabsBeingCreated[id] = true
|
newTabsBeingCreated[id] = true;
|
||||||
tabsBeingCreated = newTabsBeingCreated
|
tabsBeingCreated = newTabsBeingCreated;
|
||||||
createEmptyFile(baseDir + "/" + filePath, function() {
|
createEmptyFile(baseDir + "/" + filePath, function () {
|
||||||
newTabs[0] = {
|
newTabs[0] = {
|
||||||
id: id,
|
id: id,
|
||||||
title: I18n.tr("Untitled"),
|
title: I18n.tr("Untitled"),
|
||||||
@@ -296,110 +292,129 @@ Singleton {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
cursorPosition: 0,
|
cursorPosition: 0,
|
||||||
scrollPosition: 0
|
scrollPosition: 0
|
||||||
}
|
};
|
||||||
currentTabIndex = 0
|
currentTabIndex = 0;
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
|
|
||||||
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
delete updatedTabsBeingCreated[id]
|
delete updatedTabsBeingCreated[id];
|
||||||
tabsBeingCreated = updatedTabsBeingCreated
|
tabsBeingCreated = updatedTabsBeingCreated;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
} else {
|
} else {
|
||||||
var tabToDelete = newTabs[tabIndex]
|
var tabToDelete = newTabs[tabIndex];
|
||||||
if (tabToDelete && tabToDelete.isTemporary) {
|
if (tabToDelete && tabToDelete.isTemporary) {
|
||||||
deleteFile(baseDir + "/" + tabToDelete.filePath)
|
deleteFile(baseDir + "/" + tabToDelete.filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
newTabs.splice(tabIndex, 1)
|
newTabs.splice(tabIndex, 1);
|
||||||
if (currentTabIndex >= newTabs.length) {
|
if (currentTabIndex >= newTabs.length) {
|
||||||
currentTabIndex = newTabs.length - 1
|
currentTabIndex = newTabs.length - 1;
|
||||||
} else if (currentTabIndex > tabIndex) {
|
} else if (currentTabIndex > tabIndex) {
|
||||||
currentTabIndex -= 1
|
currentTabIndex -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToTab(tabIndex) {
|
function switchToTab(tabIndex) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
currentTabIndex = tabIndex
|
currentTabIndex = tabIndex;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorderTab(fromIndex, toIndex) {
|
function reorderTab(fromIndex, toIndex) {
|
||||||
if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length)
|
if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length)
|
||||||
return
|
return;
|
||||||
if (fromIndex === toIndex)
|
if (fromIndex === toIndex)
|
||||||
return
|
return;
|
||||||
|
var newTabs = tabs.slice();
|
||||||
var newTabs = tabs.slice()
|
var moved = newTabs.splice(fromIndex, 1)[0];
|
||||||
var moved = newTabs.splice(fromIndex, 1)[0]
|
newTabs.splice(toIndex, 0, moved);
|
||||||
newTabs.splice(toIndex, 0, moved)
|
tabs = newTabs;
|
||||||
tabs = newTabs
|
|
||||||
|
|
||||||
if (currentTabIndex === fromIndex) {
|
if (currentTabIndex === fromIndex) {
|
||||||
currentTabIndex = toIndex
|
currentTabIndex = toIndex;
|
||||||
} else if (fromIndex < currentTabIndex && toIndex >= currentTabIndex) {
|
} else if (fromIndex < currentTabIndex && toIndex >= currentTabIndex) {
|
||||||
currentTabIndex--
|
currentTabIndex--;
|
||||||
} else if (fromIndex > currentTabIndex && toIndex <= currentTabIndex) {
|
} else if (fromIndex > currentTabIndex && toIndex <= currentTabIndex) {
|
||||||
currentTabIndex++
|
currentTabIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTabAs(tabIndex, userPath) {
|
function saveTabAs(tabIndex, userPath) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
var tab = tabs[tabIndex]
|
var tab = tabs[tabIndex];
|
||||||
var fileName = userPath.split('/').pop()
|
var fileName = userPath.split('/').pop();
|
||||||
|
|
||||||
if (tab.isTemporary) {
|
if (tab.isTemporary) {
|
||||||
var tempPath = baseDir + "/" + tab.filePath
|
var tempPath = baseDir + "/" + tab.filePath;
|
||||||
copyFile(tempPath, userPath)
|
copyFile(tempPath, userPath);
|
||||||
deleteFile(tempPath)
|
deleteFile(tempPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
newTabs[tabIndex] = Object.assign({}, tab, {
|
newTabs[tabIndex] = Object.assign({}, tab, {
|
||||||
title: fileName,
|
title: fileName,
|
||||||
filePath: userPath,
|
filePath: userPath,
|
||||||
isTemporary: false,
|
isTemporary: false,
|
||||||
lastModified: new Date().toISOString()
|
lastModified: new Date().toISOString()
|
||||||
})
|
});
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameTab(tabIndex, newTitle) {
|
||||||
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
|
var trimmed = (newTitle || "").trim();
|
||||||
|
var tab = tabs[tabIndex];
|
||||||
|
if (trimmed.length === 0 || trimmed === tab.title)
|
||||||
|
return;
|
||||||
|
if (tab.isTemporary) {
|
||||||
|
updateTabMetadata(tabIndex, {
|
||||||
|
title: trimmed
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = tab.filePath.substring(0, tab.filePath.lastIndexOf('/') + 1);
|
||||||
|
var newPath = dir + trimmed;
|
||||||
|
moveFile(tab.filePath, newPath);
|
||||||
|
updateTabMetadata(tabIndex, {
|
||||||
|
title: trimmed,
|
||||||
|
filePath: newPath
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTabMetadata(tabIndex, properties) {
|
function updateTabMetadata(tabIndex, properties) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
var updatedTab = Object.assign({}, newTabs[tabIndex], properties)
|
var updatedTab = Object.assign({}, newTabs[tabIndex], properties);
|
||||||
updatedTab.lastModified = new Date().toISOString()
|
updatedTab.lastModified = new Date().toISOString();
|
||||||
newTabs[tabIndex] = updatedTab
|
newTabs[tabIndex] = updatedTab;
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTabs() {
|
function validateTabs() {
|
||||||
var validTabs = []
|
var validTabs = [];
|
||||||
for (var i = 0; i < tabs.length; i++) {
|
for (var i = 0; i < tabs.length; i++) {
|
||||||
var tab = tabs[i]
|
var tab = tabs[i];
|
||||||
validTabs.push(tab)
|
validTabs.push(tab);
|
||||||
}
|
}
|
||||||
tabs = validTabs
|
tabs = validTabs;
|
||||||
|
|
||||||
if (tabs.length === 0) {
|
if (tabs.length === 0) {
|
||||||
root.createDefaultTab()
|
root.createDefaultTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,29 +426,13 @@ Singleton {
|
|||||||
preload: true
|
preload: true
|
||||||
|
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
callback(text())
|
callback(text());
|
||||||
destroy()
|
destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoadFailed: {
|
onLoadFailed: {
|
||||||
callback("")
|
callback("");
|
||||||
destroy()
|
destroy();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: fileExistsComponent
|
|
||||||
Process {
|
|
||||||
property string path
|
|
||||||
property var callback
|
|
||||||
command: ["test", "-f", path]
|
|
||||||
|
|
||||||
Component.onCompleted: running = true
|
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
callback(exitCode === 0)
|
|
||||||
destroy()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,94 +451,46 @@ Singleton {
|
|||||||
|
|
||||||
onSaved: {
|
onSaved: {
|
||||||
if (tabIndex >= 0) {
|
if (tabIndex >= 0) {
|
||||||
root.updateTabMetadata(tabIndex, {})
|
root.updateTabMetadata(tabIndex, {});
|
||||||
}
|
}
|
||||||
if (creationCallback) {
|
if (creationCallback) {
|
||||||
creationCallback()
|
creationCallback();
|
||||||
}
|
}
|
||||||
destroy()
|
destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSaveFailed: {
|
onSaveFailed: {
|
||||||
log.error("Failed to save tab content")
|
log.error("Failed to save tab content");
|
||||||
if (creationCallback) {
|
if (creationCallback) {
|
||||||
creationCallback()
|
creationCallback();
|
||||||
}
|
}
|
||||||
destroy()
|
destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyFile(path, callback) {
|
function createEmptyFile(path, callback) {
|
||||||
var cleanPath = decodeURI(path.toString())
|
var cleanPath = decodeURI(path.toString());
|
||||||
|
|
||||||
if (!cleanPath.startsWith("/")) {
|
if (!cleanPath.startsWith("/")) {
|
||||||
cleanPath = baseDir + "/" + cleanPath
|
cleanPath = baseDir + "/" + cleanPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
var creator = fileCreatorComponent.createObject(root, {
|
Proc.runCommand("", ["touch", cleanPath], (output, exitCode) => {
|
||||||
filePath: cleanPath,
|
if (callback)
|
||||||
creationCallback: callback
|
callback();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyFile(source, destination) {
|
function copyFile(source, destination) {
|
||||||
copyProcess.source = source
|
Proc.runCommand("", ["cp", source, destination], null);
|
||||||
copyProcess.destination = destination
|
|
||||||
copyProcess.running = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteFile(path) {
|
function deleteFile(path) {
|
||||||
deleteProcess.filePath = path
|
Proc.runCommand("", ["rm", "-f", path], null);
|
||||||
deleteProcess.running = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
function moveFile(source, destination) {
|
||||||
id: fileCreatorComponent
|
Proc.runCommand("", ["mv", source, destination], null);
|
||||||
QtObject {
|
|
||||||
property string filePath
|
|
||||||
property var creationCallback
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
var touchProcess = touchProcessComponent.createObject(this, {
|
|
||||||
filePath: filePath,
|
|
||||||
callback: creationCallback
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: touchProcessComponent
|
|
||||||
Process {
|
|
||||||
property string filePath
|
|
||||||
property var callback
|
|
||||||
command: ["touch", filePath]
|
|
||||||
|
|
||||||
Component.onCompleted: running = true
|
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (callback) callback()
|
|
||||||
destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: copyProcess
|
|
||||||
property string source
|
|
||||||
property string destination
|
|
||||||
command: ["cp", source, destination]
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: deleteProcess
|
|
||||||
property string filePath
|
|
||||||
command: ["rm", "-f", filePath]
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: mkdirProcess
|
|
||||||
command: ["mkdir", "-p", root.baseDir, root.filesDir]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -850,10 +851,28 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notepadSlideoutForFocusedScreen() {
|
||||||
|
if (!notepadSlideouts || notepadSlideouts.length === 0)
|
||||||
|
return null;
|
||||||
|
const focused = BarWidgetService.getFocusedScreenName();
|
||||||
|
if (focused) {
|
||||||
|
for (var i = 0; i < notepadSlideouts.length; i++) {
|
||||||
|
if (notepadSlideouts[i]?.modelData?.name === focused)
|
||||||
|
return notepadSlideouts[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notepadSlideouts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remembered presentation wins over the configured default until the user
|
||||||
|
// changes the default in settings (handled below).
|
||||||
|
readonly property string notepadResolvedMode: SessionData.notepadLastMode || SettingsData.notepadDefaultMode
|
||||||
|
|
||||||
function openNotepadSlideout() {
|
function openNotepadSlideout() {
|
||||||
|
SessionData.setNotepadLastMode("slideout");
|
||||||
notepadPopout?.hide();
|
notepadPopout?.hide();
|
||||||
if (notepadSlideouts.length > 0) {
|
if (notepadSlideouts.length > 0) {
|
||||||
notepadSlideouts[0]?.show();
|
notepadSlideoutForFocusedScreen()?.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,6 +880,7 @@ Singleton {
|
|||||||
Connections {
|
Connections {
|
||||||
target: SettingsData
|
target: SettingsData
|
||||||
function onNotepadDefaultModeChanged() {
|
function onNotepadDefaultModeChanged() {
|
||||||
|
SessionData.setNotepadLastMode(SettingsData.notepadDefaultMode);
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (SettingsData.notepadDefaultMode === "popout") {
|
||||||
var hadSlideout = false;
|
var hadSlideout = false;
|
||||||
for (var i = 0; i < root.notepadSlideouts.length; i++) {
|
for (var i = 0; i < root.notepadSlideouts.length; i++) {
|
||||||
@@ -879,7 +899,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openNotepad() {
|
function openNotepad() {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (notepadResolvedMode === "popout") {
|
||||||
openNotepadPopout();
|
openNotepadPopout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -887,22 +907,22 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeNotepad() {
|
function closeNotepad() {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (notepadResolvedMode === "popout") {
|
||||||
notepadPopout?.hide();
|
notepadPopout?.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (notepadSlideouts.length > 0) {
|
if (notepadSlideouts.length > 0) {
|
||||||
notepadSlideouts[0]?.hide();
|
notepadSlideoutForFocusedScreen()?.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNotepad() {
|
function toggleNotepad() {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (notepadResolvedMode === "popout") {
|
||||||
toggleNotepadPopout();
|
toggleNotepadPopout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (notepadSlideouts.length > 0) {
|
if (notepadSlideouts.length > 0) {
|
||||||
notepadSlideouts[0]?.toggle();
|
notepadSlideoutForFocusedScreen()?.toggle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,6 +932,7 @@ Singleton {
|
|||||||
property string _notepadPendingOpenFilePath: ""
|
property string _notepadPendingOpenFilePath: ""
|
||||||
|
|
||||||
function openNotepadPopout() {
|
function openNotepadPopout() {
|
||||||
|
SessionData.setNotepadLastMode("popout");
|
||||||
closeNotepadSlideouts();
|
closeNotepadSlideouts();
|
||||||
if (notepadPopout) {
|
if (notepadPopout) {
|
||||||
notepadPopout.show();
|
notepadPopout.show();
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Singleton {
|
|||||||
|
|
||||||
property bool hasUwsm: false
|
property bool hasUwsm: false
|
||||||
property bool isElogind: false
|
property bool isElogind: false
|
||||||
|
property bool loginctlCommandAvailable: false
|
||||||
|
property bool systemctlCommandAvailable: false
|
||||||
property bool hibernateSupported: false
|
property bool hibernateSupported: false
|
||||||
property bool inhibitorAvailable: true
|
property bool inhibitorAvailable: true
|
||||||
property bool idleInhibited: false
|
property bool idleInhibited: false
|
||||||
@@ -52,6 +54,8 @@ Singleton {
|
|||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
detectElogindProcess.running = true;
|
detectElogindProcess.running = true;
|
||||||
|
detectLoginctlProcess.running = true;
|
||||||
|
detectSystemctlProcess.running = true;
|
||||||
detectHibernateProcess.running = true;
|
detectHibernateProcess.running = true;
|
||||||
detectPrimeRunProcess.running = true;
|
detectPrimeRunProcess.running = true;
|
||||||
if (!SettingsData.loginctlLockIntegration) {
|
if (!SettingsData.loginctlLockIntegration) {
|
||||||
@@ -87,6 +91,26 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: detectLoginctlProcess
|
||||||
|
running: false
|
||||||
|
command: ["sh", "-c", "command -v loginctl"]
|
||||||
|
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
loginctlCommandAvailable = (exitCode === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: detectSystemctlProcess
|
||||||
|
running: false
|
||||||
|
command: ["sh", "-c", "command -v systemctl"]
|
||||||
|
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
systemctlCommandAvailable = (exitCode === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: detectHibernateProcess
|
id: detectHibernateProcess
|
||||||
running: false
|
running: false
|
||||||
@@ -325,9 +349,14 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function powerManagerCommand(action) {
|
||||||
|
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
|
||||||
|
return [useLoginctl ? "loginctl" : "systemctl", action];
|
||||||
|
}
|
||||||
|
|
||||||
function suspend() {
|
function suspend() {
|
||||||
if (SettingsData.customPowerActionSuspend.length === 0) {
|
if (SettingsData.customPowerActionSuspend.length === 0) {
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"]);
|
Quickshell.execDetached(powerManagerCommand("suspend"));
|
||||||
} else {
|
} else {
|
||||||
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
|
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
|
||||||
}
|
}
|
||||||
@@ -338,13 +367,13 @@ Singleton {
|
|||||||
if (SettingsData.customPowerActionHibernate.length > 0) {
|
if (SettingsData.customPowerActionHibernate.length > 0) {
|
||||||
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
|
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
|
||||||
} else {
|
} else {
|
||||||
hibernateProcess.command = [isElogind ? "loginctl" : "systemctl", "hibernate"];
|
hibernateProcess.command = powerManagerCommand("hibernate");
|
||||||
}
|
}
|
||||||
hibernateProcess.running = true;
|
hibernateProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function suspendThenHibernate() {
|
function suspendThenHibernate() {
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend-then-hibernate"]);
|
Quickshell.execDetached(powerManagerCommand("suspend-then-hibernate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function suspendWithBehavior(behavior) {
|
function suspendWithBehavior(behavior) {
|
||||||
@@ -359,7 +388,7 @@ Singleton {
|
|||||||
|
|
||||||
function reboot() {
|
function reboot() {
|
||||||
if (SettingsData.customPowerActionReboot.length === 0) {
|
if (SettingsData.customPowerActionReboot.length === 0) {
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"]);
|
Quickshell.execDetached(powerManagerCommand("reboot"));
|
||||||
} else {
|
} else {
|
||||||
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionReboot]);
|
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionReboot]);
|
||||||
}
|
}
|
||||||
@@ -367,7 +396,7 @@ Singleton {
|
|||||||
|
|
||||||
function poweroff() {
|
function poweroff() {
|
||||||
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"]);
|
Quickshell.execDetached(powerManagerCommand("poweroff"));
|
||||||
} else {
|
} else {
|
||||||
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionPowerOff]);
|
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionPowerOff]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,18 +56,19 @@ Singleton {
|
|||||||
|
|
||||||
function loadArtwork(url) {
|
function loadArtwork(url) {
|
||||||
if (!url || url === "") {
|
if (!url || url === "") {
|
||||||
resolvedArtUrl = "";
|
// Keep stale art; only blank once the empty url debounce settles.
|
||||||
_lastArtUrl = "";
|
_lastArtUrl = "";
|
||||||
loading = false;
|
loading = false;
|
||||||
|
_clearDebounce.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_clearDebounce.stop();
|
||||||
if (url === _lastArtUrl)
|
if (url === _lastArtUrl)
|
||||||
return;
|
return;
|
||||||
_lastArtUrl = url;
|
_lastArtUrl = url;
|
||||||
|
|
||||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
loading = true;
|
loading = true;
|
||||||
resolvedArtUrl = ""; // Clear stale artwork immediately while loading
|
|
||||||
const targetUrl = url;
|
const targetUrl = url;
|
||||||
const hash = djb2Hash(url);
|
const hash = djb2Hash(url);
|
||||||
const cacheDir = Paths.strip(Paths.imagecache);
|
const cacheDir = Paths.strip(Paths.imagecache);
|
||||||
@@ -134,19 +135,30 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
resolvedArtUrl = ""; // Clear stale artwork immediately while verifying local file
|
|
||||||
const localUrl = url;
|
const localUrl = url;
|
||||||
const filePath = url.startsWith("file://") ? url.substring(7) : url;
|
const filePath = url.startsWith("file://") ? url.substring(7) : url;
|
||||||
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
|
// Cover file often lands after the metadata update, so poll briefly.
|
||||||
|
Proc.runCommand(null, ["sh", "-c", "for i in $(seq 20); do [ -f \"$1\" ] && exit 0; sleep 0.15; done; exit 1", "sh", filePath], (output, exitCode) => {
|
||||||
if (_lastArtUrl !== localUrl)
|
if (_lastArtUrl !== localUrl)
|
||||||
return;
|
return;
|
||||||
resolvedArtUrl = exitCode === 0 ? localUrl : "";
|
resolvedArtUrl = exitCode === 0 ? localUrl : "";
|
||||||
loading = false;
|
loading = false;
|
||||||
}, 200);
|
}, 50, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _clearDebounce
|
||||||
|
interval: 800
|
||||||
|
onTriggered: {
|
||||||
|
if (root._lastArtUrl === "")
|
||||||
|
root.resolvedArtUrl = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
|
||||||
|
property string _resolvedTrackKey: ""
|
||||||
|
|
||||||
onActivePlayerChanged: _updateArtUrl()
|
onActivePlayerChanged: _updateArtUrl()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -157,8 +169,21 @@ Singleton {
|
|||||||
function onMetadataChanged() { root._updateArtUrl(); }
|
function onMetadataChanged() { root._updateArtUrl(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _trackKey() {
|
||||||
|
const p = activePlayer;
|
||||||
|
if (!p)
|
||||||
|
return "";
|
||||||
|
return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || "");
|
||||||
|
}
|
||||||
|
|
||||||
function _updateArtUrl() {
|
function _updateArtUrl() {
|
||||||
const url = getArtworkUrl(activePlayer);
|
const url = getArtworkUrl(activePlayer);
|
||||||
|
const key = _trackKey();
|
||||||
|
// Ignore metadata jitter once resolved, but only when the url still matches
|
||||||
|
// (trackArtUrl can update before the title, and skipping then wedges old art).
|
||||||
|
if (key !== "" && key === _resolvedTrackKey && url === _lastArtUrl && resolvedArtUrl !== "")
|
||||||
|
return;
|
||||||
|
_resolvedTrackKey = key;
|
||||||
loadArtwork(url);
|
loadArtwork(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 qt_TexCoord0;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
float phase;
|
||||||
|
float spin;
|
||||||
|
float sizePx;
|
||||||
|
float baseRadiusPx;
|
||||||
|
float amplitudePx;
|
||||||
|
float activation;
|
||||||
|
float energy;
|
||||||
|
vec2 bandsB;
|
||||||
|
vec4 bandsA;
|
||||||
|
vec4 fillColor;
|
||||||
|
} ubuf;
|
||||||
|
|
||||||
|
const float TAU = 6.28318530718;
|
||||||
|
const float NOISE_FREQ = 1.25;
|
||||||
|
const float ORBIT_RADIUS = 1.0;
|
||||||
|
const float WARP_STRENGTH = 0.75;
|
||||||
|
const float RING_BIAS = 0.25;
|
||||||
|
const float MID_FREQ = 2.4;
|
||||||
|
const float MID_GAIN = 0.34;
|
||||||
|
const float HIGH_FREQ = 5.0;
|
||||||
|
const float HIGH_GAIN = 0.16;
|
||||||
|
const float LOBE_MAX = 1.3;
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
p = fract(p * vec2(123.34, 456.21));
|
||||||
|
p += dot(p, p + 45.32);
|
||||||
|
return fract(p.x * p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float vnoise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
float a = hash(i);
|
||||||
|
float b = hash(i + vec2(1.0, 0.0));
|
||||||
|
float c = hash(i + vec2(0.0, 1.0));
|
||||||
|
float d = hash(i + vec2(1.0, 1.0));
|
||||||
|
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float fbm(vec2 p) {
|
||||||
|
return 0.55 * vnoise(p) + 0.3 * vnoise(p * 2.0 + vec2(17.1, 9.2)) + 0.15 * vnoise(p * 3.9 + vec2(4.3, 21.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 p = (qt_TexCoord0 - 0.5) * ubuf.sizePx;
|
||||||
|
float r = length(p);
|
||||||
|
float aa = max(fwidth(r), 0.75);
|
||||||
|
|
||||||
|
float T = ubuf.phase * TAU;
|
||||||
|
vec2 dir = r > 0.001 ? p / r : vec2(1.0, 0.0);
|
||||||
|
float cs = cos(ubuf.spin);
|
||||||
|
float sn = sin(ubuf.spin);
|
||||||
|
dir = vec2(dir.x * cs - dir.y * sn, dir.x * sn + dir.y * cs);
|
||||||
|
vec2 q = dir * NOISE_FREQ;
|
||||||
|
|
||||||
|
vec2 orbitA = vec2(cos(T), sin(T)) * ORBIT_RADIUS;
|
||||||
|
vec2 orbitB = vec2(cos(2.0 * T + 2.1), sin(2.0 * T + 2.1)) * (ORBIT_RADIUS * 0.6);
|
||||||
|
float w1 = fbm(q + orbitA);
|
||||||
|
float w2 = fbm(q * 1.3 + orbitB + vec2(3.7, 7.3));
|
||||||
|
float n = fbm(q + orbitA + WARP_STRENGTH * (vec2(w1, w2) - 0.5));
|
||||||
|
|
||||||
|
float mid = max(ubuf.bandsA.z, ubuf.bandsA.w);
|
||||||
|
float high = max(ubuf.bandsB.x, ubuf.bandsB.y);
|
||||||
|
|
||||||
|
vec2 orbitM = vec2(cos(3.0 * T + 0.7), sin(3.0 * T + 0.7)) * 0.9;
|
||||||
|
vec2 orbitH = vec2(cos(5.0 * T + 4.2), sin(5.0 * T + 4.2)) * 1.2;
|
||||||
|
float midTerm = (vnoise(dir * MID_FREQ + orbitM) - 0.5) * MID_GAIN * mid;
|
||||||
|
float highTerm = (vnoise(dir * HIGH_FREQ + orbitH) - 0.5) * HIGH_GAIN * high;
|
||||||
|
|
||||||
|
float shaped = smoothstep(0.34, 0.7, n);
|
||||||
|
float body = ubuf.energy * (RING_BIAS + (1.0 - RING_BIAS) * shaped);
|
||||||
|
float lobe = clamp(body + midTerm + highTerm, 0.0, LOBE_MAX);
|
||||||
|
float blobR = ubuf.baseRadiusPx + ubuf.activation * ubuf.amplitudePx * lobe;
|
||||||
|
|
||||||
|
float mask = 1.0 - smoothstep(-aa, aa, r - blobR);
|
||||||
|
float a = ubuf.fillColor.a * mask * ubuf.qt_Opacity;
|
||||||
|
fragColor = vec4(ubuf.fillColor.rgb * a, a);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 qt_TexCoord0;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
float widthPx;
|
||||||
|
float heightPx;
|
||||||
|
float minH;
|
||||||
|
float maxH;
|
||||||
|
vec2 bandsB;
|
||||||
|
vec4 bandsA;
|
||||||
|
vec4 fillColor;
|
||||||
|
} ubuf;
|
||||||
|
|
||||||
|
const float BAR_W = 2.0;
|
||||||
|
const float GAP = 1.5;
|
||||||
|
const float RADIUS = 1.0;
|
||||||
|
|
||||||
|
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
|
||||||
|
vec2 q = abs(p) - halfSize + vec2(r);
|
||||||
|
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 px = vec2(qt_TexCoord0.x * ubuf.widthPx, qt_TexCoord0.y * ubuf.heightPx);
|
||||||
|
float total = 6.0 * BAR_W + 5.0 * GAP;
|
||||||
|
float x0 = (ubuf.widthPx - total) * 0.5;
|
||||||
|
float slot = BAR_W + GAP;
|
||||||
|
float fi = floor((px.x - x0) / slot);
|
||||||
|
|
||||||
|
if (fi < 0.0 || fi > 5.0) {
|
||||||
|
fragColor = vec4(0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = int(fi);
|
||||||
|
float lvl = i == 0 ? ubuf.bandsA.x : i == 1 ? ubuf.bandsA.y : i == 2 ? ubuf.bandsA.z : i == 3 ? ubuf.bandsA.w : i == 4 ? ubuf.bandsB.x : ubuf.bandsB.y;
|
||||||
|
float h = ubuf.minH + clamp(lvl, 0.0, 1.0) * (ubuf.maxH - ubuf.minH);
|
||||||
|
|
||||||
|
float lx = px.x - x0 - fi * slot;
|
||||||
|
vec2 p = vec2(lx - BAR_W * 0.5, px.y - ubuf.heightPx * 0.5);
|
||||||
|
float d = sdRoundBar(p, vec2(BAR_W * 0.5, h * 0.5), RADIUS);
|
||||||
|
float mask = 1.0 - smoothstep(-0.6, 0.6, d);
|
||||||
|
|
||||||
|
float a = ubuf.fillColor.a * mask * ubuf.qt_Opacity;
|
||||||
|
fragColor = vec4(ubuf.fillColor.rgb * a, a);
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 qt_TexCoord0;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
float widthPx;
|
||||||
|
float heightPx;
|
||||||
|
float value;
|
||||||
|
float actualValue;
|
||||||
|
float phase;
|
||||||
|
float ampPx;
|
||||||
|
float wavelengthPx;
|
||||||
|
float lineWidthPx;
|
||||||
|
float showActual;
|
||||||
|
vec4 fillColor;
|
||||||
|
vec4 trackColor;
|
||||||
|
vec4 playheadColor;
|
||||||
|
vec4 actualColor;
|
||||||
|
} ubuf;
|
||||||
|
|
||||||
|
const float TAU = 6.28318530718;
|
||||||
|
const float AA = 0.75; // pixel-space antialias band
|
||||||
|
|
||||||
|
// Signed distance to a rounded box centered at the origin.
|
||||||
|
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
|
||||||
|
vec2 q = abs(p) - halfSize + vec2(r);
|
||||||
|
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite a straight-alpha color over a premultiplied accumulator.
|
||||||
|
vec4 blendOver(vec4 dst, vec3 rgb, float a) {
|
||||||
|
return vec4(rgb * a + dst.rgb * (1.0 - a), a + dst.a * (1.0 - a));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float w = ubuf.widthPx;
|
||||||
|
float h = ubuf.heightPx;
|
||||||
|
vec2 px = vec2(qt_TexCoord0.x * w, qt_TexCoord0.y * h);
|
||||||
|
|
||||||
|
float mid = h * 0.5;
|
||||||
|
float halfW = ubuf.lineWidthPx * 0.5;
|
||||||
|
float k = TAU / max(ubuf.wavelengthPx, 1e-3);
|
||||||
|
|
||||||
|
float playX = clamp(ubuf.value, 0.0, 1.0) * w;
|
||||||
|
float actualX = clamp(ubuf.actualValue, 0.0, 1.0) * w;
|
||||||
|
bool seeking = ubuf.showActual > 0.5;
|
||||||
|
|
||||||
|
float loX = min(playX, actualX);
|
||||||
|
float hiX = max(playX, actualX);
|
||||||
|
float fillEnd = seeking ? loX : playX; // filled progress ends here
|
||||||
|
float actStart = seeking ? loX : playX; // seek-preview segment
|
||||||
|
float actEnd = seeking ? hiX : playX;
|
||||||
|
float trackStart = seeking ? hiX : playX; // unplayed remainder
|
||||||
|
|
||||||
|
// Perpendicular distance to the animated sine stroke.
|
||||||
|
float ang = k * px.x + ubuf.phase;
|
||||||
|
float wy = mid + ubuf.ampPx * sin(ang);
|
||||||
|
float slope = ubuf.ampPx * k * cos(ang);
|
||||||
|
float dWave = abs(px.y - wy) / sqrt(1.0 + slope * slope);
|
||||||
|
float aaW = AA;
|
||||||
|
float waveStroke = 1.0 - smoothstep(halfW - aaW, halfW + aaW, dWave);
|
||||||
|
|
||||||
|
// Straight remainder line.
|
||||||
|
float dLine = abs(px.y - mid);
|
||||||
|
float aaL = AA;
|
||||||
|
float lineStroke = 1.0 - smoothstep(halfW - aaL, halfW + aaL, dLine);
|
||||||
|
|
||||||
|
vec4 col = vec4(0.0);
|
||||||
|
|
||||||
|
// 1. Track (unplayed remainder), to the right of the progress head.
|
||||||
|
{
|
||||||
|
float m = lineStroke * step(trackStart, px.x);
|
||||||
|
col = blendOver(col, ubuf.trackColor.rgb, ubuf.trackColor.a * m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Seek-preview segment (only while seeking).
|
||||||
|
if (seeking) {
|
||||||
|
float m = waveStroke * step(actStart, px.x) * step(px.x, actEnd);
|
||||||
|
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Filled progress wave.
|
||||||
|
{
|
||||||
|
float m = waveStroke * step(halfW, px.x) * step(px.x, fillEnd);
|
||||||
|
// Rounded start cap.
|
||||||
|
float capS = length(px - vec2(halfW, mid + ubuf.ampPx * sin(k * halfW + ubuf.phase))) - halfW;
|
||||||
|
float capM = 1.0 - smoothstep(-aaW, aaW, capS);
|
||||||
|
m = max(m, capM * step(halfW - 1.0, px.x));
|
||||||
|
col = blendOver(col, ubuf.fillColor.rgb, ubuf.fillColor.a * m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Actual-position marker (only while seeking).
|
||||||
|
if (seeking) {
|
||||||
|
float amH = max(ubuf.lineWidthPx + 4.0, 10.0);
|
||||||
|
float d = sdRoundBar(px - vec2(actualX, mid), vec2(1.0, amH * 0.5), 1.0);
|
||||||
|
float aa = AA;
|
||||||
|
float m = 1.0 - smoothstep(-aa, aa, d);
|
||||||
|
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Playhead pill (on top).
|
||||||
|
{
|
||||||
|
float phW = 3.5;
|
||||||
|
float phH = max(ubuf.lineWidthPx + 12.0, 16.0);
|
||||||
|
float d = sdRoundBar(px - vec2(playX, mid), vec2(phW * 0.5, phH * 0.5), phW * 0.5);
|
||||||
|
float aa = AA;
|
||||||
|
float m = 1.0 - smoothstep(-aa, aa, d);
|
||||||
|
col = blendOver(col, ubuf.playheadColor.rgb, ubuf.playheadColor.a * m);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor = col * ubuf.qt_Opacity;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -85,6 +85,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
imagePath: root.imagePath
|
imagePath: root.imagePath
|
||||||
maxCacheSize: root.iconSize * 2
|
maxCacheSize: root.iconSize * 2
|
||||||
|
animate: false
|
||||||
visible: root.isImage && status === Image.Ready
|
visible: root.isImage && status === Image.Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+124
-108
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Shapes
|
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -15,6 +14,36 @@ Item {
|
|||||||
property bool showAnimation: true
|
property bool showAnimation: true
|
||||||
property real animationScale: 1.0
|
property real animationScale: 1.0
|
||||||
|
|
||||||
|
readonly property real blobBaseRadiusFactor: 0.43
|
||||||
|
readonly property real blobAmplitudeFactor: 0.115
|
||||||
|
readonly property real blobOvershoot: 1.15
|
||||||
|
readonly property real blobEnergySensitivity: 1.15
|
||||||
|
readonly property real cavaFullScale: 45
|
||||||
|
readonly property real blobAttack: 0.75
|
||||||
|
readonly property real blobRelease: 0.2
|
||||||
|
readonly property real blobBeatBoost: 2.5
|
||||||
|
readonly property real blobBeatKick: 4
|
||||||
|
readonly property real blobOnsetThreshold: 1.4
|
||||||
|
readonly property real blobSpringStiffness: 220
|
||||||
|
readonly property real blobSpringDamping: 19
|
||||||
|
readonly property real blobMorphSpeed: 0.05
|
||||||
|
readonly property real blobMorphBoost: 1.7
|
||||||
|
readonly property real blobSpinSpeed: 0.03
|
||||||
|
|
||||||
|
readonly property bool blobActive: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation && albumArtStatus === Image.Ready
|
||||||
|
property var smoothedBands: [0, 0, 0, 0, 0, 0]
|
||||||
|
property var slowBands: [0, 0, 0, 0, 0, 0]
|
||||||
|
property var bandTargets: [0, 0, 0, 0, 0, 0]
|
||||||
|
property var bandDisplay: [0, 0, 0, 0, 0, 0]
|
||||||
|
property var prevLevels: [0, 0, 0, 0, 0, 0]
|
||||||
|
readonly property var fluxWeights: [1.0, 1.0, 0.6, 0.6, 0.35, 0.35]
|
||||||
|
property real fluxAvg: 0.02
|
||||||
|
property real loudCtx: 0.1
|
||||||
|
property int beatCooldown: 0
|
||||||
|
property real energyTarget: 0
|
||||||
|
property real energyPos: 0
|
||||||
|
property real energyVel: 0
|
||||||
|
|
||||||
onActivePlayerChanged: {
|
onActivePlayerChanged: {
|
||||||
lastValidArtUrl = "";
|
lastValidArtUrl = "";
|
||||||
}
|
}
|
||||||
@@ -25,6 +54,62 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateBands() {
|
||||||
|
const vals = CavaService.values;
|
||||||
|
if (!vals || vals.length < 6)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const s = smoothedBands;
|
||||||
|
const slow = slowBands;
|
||||||
|
const out = bandTargets;
|
||||||
|
const prev = prevLevels;
|
||||||
|
const w = fluxWeights;
|
||||||
|
let flux = 0;
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const level = Math.min(Math.max(vals[i], 0), cavaFullScale) / cavaFullScale;
|
||||||
|
flux += Math.max(0, level - prev[i]) * w[i];
|
||||||
|
prev[i] = level;
|
||||||
|
const alpha = level > s[i] ? blobAttack : blobRelease;
|
||||||
|
s[i] += alpha * (level - s[i]);
|
||||||
|
slow[i] += 0.05 * (level - slow[i]);
|
||||||
|
const punch = Math.max(0, s[i] - slow[i]) * blobBeatBoost;
|
||||||
|
out[i] = Math.min(1, (0.55 * s[i] + punch) * blobEnergySensitivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = flux / Math.max(fluxAvg, 0.004);
|
||||||
|
fluxAvg += 0.06 * (flux - fluxAvg);
|
||||||
|
if (beatCooldown > 0) {
|
||||||
|
beatCooldown--;
|
||||||
|
} else if (ratio > blobOnsetThreshold && flux > 0.008) {
|
||||||
|
energyVel += blobBeatKick * Math.min(2.5, ratio - 1);
|
||||||
|
beatCooldown = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loud = 0.7 * Math.max(prev[0], prev[1]) + 0.3 * Math.max(prev[2], prev[3]);
|
||||||
|
loudCtx += 0.03 * (loud - loudCtx);
|
||||||
|
const surge = Math.max(0, loud / Math.max(loudCtx, 0.05) - 1);
|
||||||
|
energyTarget = Math.min(1, 0.5 * loud + 0.6 * Math.min(1, surge));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stepBlob(dt) {
|
||||||
|
energyVel += (blobSpringStiffness * (energyTarget - energyPos) - blobSpringDamping * energyVel) * dt;
|
||||||
|
energyPos = Math.max(0, Math.min(blobOvershoot, energyPos + energyVel * dt));
|
||||||
|
blobEffect.energy = energyPos;
|
||||||
|
|
||||||
|
const d = bandDisplay;
|
||||||
|
const t = bandTargets;
|
||||||
|
const f = Math.min(1, dt * 14);
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
d[i] += f * (t[i] - d[i]);
|
||||||
|
}
|
||||||
|
blobEffect.bandsA = Qt.vector4d(d[0], d[1], d[2], d[3]);
|
||||||
|
blobEffect.bandsB = Qt.vector2d(d[4], d[5]);
|
||||||
|
|
||||||
|
const speed = 1 + energyPos * blobMorphBoost;
|
||||||
|
blobEffect.phase = (blobEffect.phase + dt * blobMorphSpeed * speed) % 1;
|
||||||
|
blobEffect.spin = (blobEffect.spin + dt * blobSpinSpeed) % 6.28318530718;
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
@@ -34,119 +119,50 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Shape {
|
|
||||||
id: morphingBlob
|
|
||||||
width: parent.width * 1.1
|
|
||||||
height: parent.height * 1.1
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
|
||||||
asynchronous: false
|
|
||||||
antialiasing: true
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
z: 0
|
|
||||||
layer.enabled: false
|
|
||||||
|
|
||||||
readonly property real centerX: width / 2
|
|
||||||
readonly property real centerY: height / 2
|
|
||||||
readonly property real baseRadius: Math.min(width, height) * 0.41 * root.animationScale
|
|
||||||
readonly property int segments: 28
|
|
||||||
|
|
||||||
property var audioLevels: {
|
|
||||||
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
|
|
||||||
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6];
|
|
||||||
}
|
|
||||||
return CavaService.values;
|
|
||||||
}
|
|
||||||
|
|
||||||
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
|
|
||||||
property var cubics: []
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CavaService
|
target: CavaService
|
||||||
|
enabled: blobEffect.visible
|
||||||
function onValuesChanged() {
|
function onValuesChanged() {
|
||||||
if (morphingBlob.visible) {
|
root.updateBands();
|
||||||
morphingBlob.updatePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: cubicSegment
|
|
||||||
PathCubic {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: pathMoveComp
|
|
||||||
PathMove {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
shapePath.pathElements.push(pathMoveComp.createObject(shapePath));
|
|
||||||
|
|
||||||
for (let i = 0; i < segments; i++) {
|
|
||||||
const seg = cubicSegment.createObject(shapePath);
|
|
||||||
shapePath.pathElements.push(seg);
|
|
||||||
cubics.push(seg);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePath() {
|
|
||||||
if (cubics.length === 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const alpha = 0.35;
|
|
||||||
const minLen = Math.min(smoothedLevels.length, audioLevels.length);
|
|
||||||
for (let i = 0; i < minLen; i++) {
|
|
||||||
smoothedLevels[i] += alpha * (audioLevels[i] - smoothedLevels[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const angleStep = 2 * Math.PI / segments;
|
|
||||||
const tension3 = 0.16666667;
|
|
||||||
const startMove = shapePath.pathElements[0];
|
|
||||||
|
|
||||||
const points = new Array(segments);
|
|
||||||
for (let i = 0; i < segments; i++) {
|
|
||||||
const angle = i * angleStep;
|
|
||||||
const audioIndex = i % 10;
|
|
||||||
const rawLevel = smoothedLevels[audioIndex] || 0;
|
|
||||||
const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel);
|
|
||||||
const audioLevel = Math.max(0.15, Math.sqrt(clampedLevel * 0.01)) * 0.5;
|
|
||||||
const radius = baseRadius * (1.0 + audioLevel);
|
|
||||||
points[i] = {
|
|
||||||
x: centerX + Math.cos(angle) * radius,
|
|
||||||
y: centerY + Math.sin(angle) * radius
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
startMove.x = points[0].x;
|
|
||||||
startMove.y = points[0].y;
|
|
||||||
|
|
||||||
for (let i = 0; i < segments; i++) {
|
|
||||||
const p0 = points[(i + segments - 1) % segments];
|
|
||||||
const p1 = points[i];
|
|
||||||
const p2 = points[(i + 1) % segments];
|
|
||||||
const p3 = points[(i + 2) % segments];
|
|
||||||
|
|
||||||
const seg = cubics[i];
|
|
||||||
seg.control1X = p1.x + (p2.x - p0.x) * tension3;
|
|
||||||
seg.control1Y = p1.y + (p2.y - p0.y) * tension3;
|
|
||||||
seg.control2X = p2.x - (p3.x - p1.x) * tension3;
|
|
||||||
seg.control2Y = p2.y - (p3.y - p1.y) * tension3;
|
|
||||||
seg.x = p2.x;
|
|
||||||
seg.y = p2.y;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapePath {
|
FrameAnimation {
|
||||||
id: shapePath
|
running: blobEffect.visible
|
||||||
fillColor: Theme.primary
|
onTriggered: root.stepBlob(Math.min(frameTime, 0.05))
|
||||||
strokeColor: "transparent"
|
|
||||||
strokeWidth: 0
|
|
||||||
joinStyle: ShapePath.RoundJoin
|
|
||||||
fillRule: ShapePath.WindingFill
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShaderEffect {
|
||||||
|
id: blobEffect
|
||||||
|
|
||||||
|
readonly property real span: Math.min(root.width, root.height)
|
||||||
|
|
||||||
|
width: span * (root.blobBaseRadiusFactor + root.blobAmplitudeFactor * root.blobOvershoot) * 2 * root.animationScale + 4
|
||||||
|
height: width
|
||||||
|
anchors.centerIn: parent
|
||||||
|
z: 0
|
||||||
|
visible: root.blobActive || activation > 0.004
|
||||||
|
|
||||||
|
property real phase: 0
|
||||||
|
property real spin: 0
|
||||||
|
property real sizePx: width
|
||||||
|
property real baseRadiusPx: span * root.blobBaseRadiusFactor * root.animationScale
|
||||||
|
property real amplitudePx: span * root.blobAmplitudeFactor * root.animationScale
|
||||||
|
property real activation: root.blobActive ? 1 : 0
|
||||||
|
property real energy: 0
|
||||||
|
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
|
||||||
|
property vector2d bandsB: Qt.vector2d(0, 0)
|
||||||
|
readonly property color accentColor: MediaAccentService.accent
|
||||||
|
property vector4d fillColor: Qt.vector4d(accentColor.r, accentColor.g, accentColor.b, accentColor.a)
|
||||||
|
|
||||||
|
Behavior on activation {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 550
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/blob.frag.qsb")
|
||||||
}
|
}
|
||||||
|
|
||||||
DankCircularImage {
|
DankCircularImage {
|
||||||
@@ -158,7 +174,7 @@ Item {
|
|||||||
|
|
||||||
imageSource: artUrl || lastValidArtUrl || ""
|
imageSource: artUrl || lastValidArtUrl || ""
|
||||||
fallbackIcon: "album"
|
fallbackIcon: "album"
|
||||||
border.color: Theme.primary
|
border.color: MediaAccentService.accent
|
||||||
border.width: 2
|
border.width: 2
|
||||||
|
|
||||||
onImageSourceChanged: {
|
onImageSourceChanged: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Window
|
import QtQuick.Window
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -13,9 +13,17 @@ Rectangle {
|
|||||||
property bool cacheImages: true
|
property bool cacheImages: true
|
||||||
property bool hasImage: imageSource !== ""
|
property bool hasImage: imageSource !== ""
|
||||||
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
|
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
|
||||||
// Probe with AnimatedImage first; once loaded, check frameCount to decide.
|
|
||||||
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
|
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
|
||||||
readonly property var activeImage: isAnimated ? probe : staticImage
|
readonly property bool probeSettled: probe.status === Image.Ready || probe.status === Image.Error
|
||||||
|
readonly property var activeImage: {
|
||||||
|
if (isAnimated)
|
||||||
|
return probe;
|
||||||
|
if (staticImage.status === Image.Ready)
|
||||||
|
return staticImage;
|
||||||
|
if (probe.status === Image.Ready && probe.source !== "")
|
||||||
|
return probe;
|
||||||
|
return staticImage;
|
||||||
|
}
|
||||||
property int imageStatus: activeImage.status
|
property int imageStatus: activeImage.status
|
||||||
|
|
||||||
signal imageSaved(string filePath)
|
signal imageSaved(string filePath)
|
||||||
@@ -57,93 +65,44 @@ Rectangle {
|
|||||||
border.color: "transparent"
|
border.color: "transparent"
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
// Probe: loads as AnimatedImage to detect frame count.
|
ClippingRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
radius: Math.min(width, height) / 2
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
|
||||||
AnimatedImage {
|
AnimatedImage {
|
||||||
id: probe
|
id: probe
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
smooth: true
|
smooth: true
|
||||||
mipmap: true
|
mipmap: true
|
||||||
cache: root.cacheImages
|
cache: root.cacheImages
|
||||||
visible: false
|
visible: root.activeImage === probe && probe.status === Image.Ready && root.imageSource !== ""
|
||||||
source: root.shouldProbe ? root.imageSource : ""
|
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static fallback: used once probe confirms the image is not animated.
|
// Takes over once the probe settles on a non-animated image, then latches.
|
||||||
Image {
|
Image {
|
||||||
id: staticImage
|
id: staticImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
smooth: true
|
smooth: true
|
||||||
mipmap: true
|
mipmap: true
|
||||||
cache: root.cacheImages
|
cache: root.cacheImages
|
||||||
visible: false
|
visible: root.activeImage === staticImage && staticImage.status === Image.Ready && root.imageSource !== ""
|
||||||
sourceSize.width: Math.max(width * 2, 128)
|
sourceSize.width: Math.max(width * 2, 128)
|
||||||
sourceSize.height: Math.max(height * 2, 128)
|
sourceSize.height: Math.max(height * 2, 128)
|
||||||
source: !root.shouldProbe ? root.imageSource : ""
|
source: {
|
||||||
}
|
|
||||||
|
|
||||||
// Once the probe loads, if not animated, hand off to Image and unload probe.
|
|
||||||
Connections {
|
|
||||||
target: probe
|
|
||||||
function onStatusChanged() {
|
|
||||||
if (!root.shouldProbe)
|
if (!root.shouldProbe)
|
||||||
return;
|
return root.imageSource;
|
||||||
switch (probe.status) {
|
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
|
||||||
case Image.Ready:
|
return root.imageSource;
|
||||||
if (probe.frameCount <= 1) {
|
return "";
|
||||||
staticImage.source = root.imageSource;
|
|
||||||
probe.source = "";
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case Image.Error:
|
|
||||||
staticImage.source = root.imageSource;
|
|
||||||
probe.source = "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If imageSource changes, reset: re-probe with AnimatedImage.
|
|
||||||
onImageSourceChanged: {
|
|
||||||
if (root.shouldProbe) {
|
|
||||||
staticImage.source = "";
|
|
||||||
probe.source = root.imageSource;
|
|
||||||
} else {
|
|
||||||
probe.source = "";
|
|
||||||
staticImage.source = root.imageSource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
source: root.activeImage
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: circularMask
|
|
||||||
visible: root.activeImage.status === Image.Ready && root.imageSource !== ""
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: circularMask
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - 4
|
|
||||||
height: parent.height - 4
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1249,14 +1249,14 @@ Item {
|
|||||||
// Fast fade duration for superseded close.
|
// Fast fade duration for superseded close.
|
||||||
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
|
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
|
||||||
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0))
|
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0))
|
||||||
property real publishedOpacity: _targetOpacity
|
readonly property real publishedOpacity: opacity
|
||||||
|
|
||||||
opacity: _targetOpacity
|
opacity: _targetOpacity
|
||||||
visible: _renderActive
|
visible: _renderActive
|
||||||
|
|
||||||
scale: contentContainer.scaleValue
|
scale: contentContainer.scaleValue
|
||||||
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr)
|
x: Theme.snap(contentContainer.animX, root.dpr)
|
||||||
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr)
|
y: Theme.snap(contentContainer.animY, root.dpr)
|
||||||
|
|
||||||
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
|
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
|
||||||
layer.smooth: false
|
layer.smooth: false
|
||||||
@@ -1276,15 +1276,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on publishedOpacity {
|
|
||||||
enabled: contentWrapper._fadeWithOpacity
|
|
||||||
NumberAnimation {
|
|
||||||
duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
|
|||||||
@@ -220,6 +220,31 @@ Item {
|
|||||||
onTriggered: root._bgCommitWindow = false
|
onTriggered: root._bgCommitWindow = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An idle layer surface won't commit the cleared blur region on auto-close, so the
|
||||||
|
// blur sticks; pulse updatesEnabled false->true to force a re-commit.
|
||||||
|
property bool _blurCommitSuppress: false
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: blurCommitPulseTimer
|
||||||
|
interval: 16
|
||||||
|
onTriggered: root._blurCommitSuppress = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pulseBlurCommit() {
|
||||||
|
if (!backgroundWindow.visible)
|
||||||
|
return;
|
||||||
|
_blurCommitSuppress = true;
|
||||||
|
blurCommitPulseTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: overlayLoader.item
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onOverlayBlurActiveChanged() {
|
||||||
|
root._pulseBlurCommit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
||||||
const newX = Theme.snap(bodyX, dpr);
|
const newX = Theme.snap(bodyX, dpr);
|
||||||
const newY = Theme.snap(bodyY, dpr);
|
const newY = Theme.snap(bodyY, dpr);
|
||||||
@@ -540,7 +565,7 @@ Item {
|
|||||||
// Skip buffer updates when there's nothing to render. Briefly flipped
|
// Skip buffer updates when there's nothing to render. Briefly flipped
|
||||||
// true via _bgCommitWindow when _surfaceBodyW/H changes so the
|
// true via _bgCommitWindow when _surfaceBodyW/H changes so the
|
||||||
// contentHoleRect mask carve-out actually commits to the compositor.
|
// contentHoleRect mask carve-out actually commits to the compositor.
|
||||||
updatesEnabled: root.overlayContent !== null || root._bgCommitWindow
|
updatesEnabled: !root._blurCommitSuppress && (root.overlayContent !== null || root._bgCommitWindow)
|
||||||
|
|
||||||
WlrLayershell.namespace: root.layerNamespace + ":background"
|
WlrLayershell.namespace: root.layerNamespace + ":background"
|
||||||
WlrLayershell.layer: root.effectivePopoutLayer
|
WlrLayershell.layer: root.effectivePopoutLayer
|
||||||
@@ -620,13 +645,15 @@ Item {
|
|||||||
id: popoutBlur
|
id: popoutBlur
|
||||||
targetWindow: contentWindow
|
targetWindow: contentWindow
|
||||||
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
||||||
|
readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.08) * 1.6))
|
||||||
|
readonly property real visibleScale: s * op
|
||||||
readonly property bool revealClipActive: root.fluidStandaloneActive
|
readonly property bool revealClipActive: root.fluidStandaloneActive
|
||||||
|
|
||||||
// Blur tracks the surface's scaled rect, matching the connected backend
|
// Blur tracks the surface's scaled rect, matching the connected backend
|
||||||
blurX: revealClipActive ? contentContainer.x : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
|
blurX: revealClipActive ? contentContainer.x : contentContainer.x + contentContainer.width * (1 - visibleScale) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
|
||||||
blurY: revealClipActive ? contentContainer.y : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
|
blurY: revealClipActive ? contentContainer.y : contentContainer.y + contentContainer.height * (1 - visibleScale) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
|
||||||
blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * s) : 0
|
blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * visibleScale) : 0
|
||||||
blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * s) : 0
|
blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * visibleScale) : 0
|
||||||
blurRadius: Theme.cornerRadius
|
blurRadius: Theme.cornerRadius
|
||||||
clipEnabled: revealClipActive
|
clipEnabled: revealClipActive
|
||||||
clipX: contentContainer.x + contentContainer.revealX
|
clipX: contentContainer.x + contentContainer.revealX
|
||||||
@@ -742,8 +769,7 @@ Item {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: morph
|
id: morph
|
||||||
property real openProgress: 0
|
property real openProgress: 0
|
||||||
onOpenProgressChanged: if (root.fluidStandaloneActive)
|
onOpenProgressChanged: root._kickBlurCommit()
|
||||||
root._kickBlurCommit()
|
|
||||||
Behavior on openProgress {
|
Behavior on openProgress {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -846,7 +872,7 @@ Item {
|
|||||||
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||||
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||||
|
|
||||||
layer.enabled: !Theme.isDirectionalEffect && publishedOpacity < 1
|
layer.enabled: !Theme.isDirectionalEffect && _renderActive
|
||||||
layer.smooth: false
|
layer.smooth: false
|
||||||
layer.textureSize: Qt.size(0, 0)
|
layer.textureSize: Qt.size(0, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,10 @@ Item {
|
|||||||
value: root.value
|
value: root.value
|
||||||
actualValue: root.playerValue
|
actualValue: root.playerValue
|
||||||
showActualPlaybackState: root.isSeeking
|
showActualPlaybackState: root.isSeeking
|
||||||
actualProgressColor: Theme.onSurface_38
|
fillColor: MediaAccentService.accent
|
||||||
|
playheadColor: MediaAccentService.accent
|
||||||
|
trackColor: MediaAccentService.accentTrack
|
||||||
|
actualProgressColor: MediaAccentService.accentSubtle
|
||||||
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -179,8 +182,8 @@ Item {
|
|||||||
Item {
|
Item {
|
||||||
property real lineWidth: 3
|
property real lineWidth: 3
|
||||||
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
|
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
|
||||||
property color fillColor: Theme.primary
|
property color fillColor: MediaAccentService.accent
|
||||||
property color playheadColor: Theme.primary
|
property color playheadColor: MediaAccentService.accent
|
||||||
property color actualProgressColor: Theme.onSurface_38
|
property color actualProgressColor: Theme.onSurface_38
|
||||||
readonly property real midY: height / 2
|
readonly property real midY: height / 2
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,11 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapIndicator() {
|
||||||
|
indicator.initialSetupComplete = false;
|
||||||
|
updateIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
onCurrentIndexChanged: {
|
||||||
Qt.callLater(updateIndicator);
|
Qt.callLater(updateIndicator);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
|
||||||
|
// Wave progress indicator: track, animated fill, seek preview and playhead are
|
||||||
|
// all drawn in a single fragment shader (Shaders/frag/wave_progress.frag).
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
@@ -19,19 +20,6 @@ Item {
|
|||||||
property color playheadColor: Theme.primary
|
property color playheadColor: Theme.primary
|
||||||
property color actualProgressColor: Theme.onSurface_38
|
property color actualProgressColor: Theme.onSurface_38
|
||||||
|
|
||||||
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
|
|
||||||
function snap(v) {
|
|
||||||
return Math.round(v * dpr) / dpr;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real playX: snap(root.width * root.value)
|
|
||||||
readonly property real actualX: snap(root.width * root.actualValue)
|
|
||||||
readonly property real midY: snap(height / 2)
|
|
||||||
readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
|
|
||||||
readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
|
|
||||||
readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
|
|
||||||
readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
|
|
||||||
|
|
||||||
Behavior on currentAmp {
|
Behavior on currentAmp {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 300
|
duration: 300
|
||||||
@@ -39,236 +27,34 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
||||||
|
Component.onCompleted: currentAmp = isPlaying ? amp : 0
|
||||||
|
|
||||||
Shape {
|
ShaderEffect {
|
||||||
id: flatTrack
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
antialiasing: true
|
blending: true
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
layer.enabled: true
|
|
||||||
|
|
||||||
ShapePath {
|
readonly property real widthPx: width
|
||||||
strokeColor: root.trackColor
|
readonly property real heightPx: height
|
||||||
strokeWidth: snap(root.lineWidth)
|
readonly property real value: root.value
|
||||||
capStyle: ShapePath.RoundCap
|
readonly property real actualValue: root.actualValue
|
||||||
joinStyle: ShapePath.RoundJoin
|
readonly property real phase: root.phase
|
||||||
fillColor: "transparent"
|
readonly property real ampPx: root.currentAmp
|
||||||
PathMove {
|
readonly property real wavelengthPx: root.wavelength
|
||||||
id: flatStart
|
readonly property real lineWidthPx: root.lineWidth
|
||||||
x: Math.min(root.width, snap(root.playX + playhead.width / 2))
|
readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0
|
||||||
y: root.midY
|
readonly property color fillColor: root.fillColor
|
||||||
}
|
readonly property color trackColor: root.trackColor
|
||||||
PathLine {
|
readonly property color playheadColor: root.playheadColor
|
||||||
id: flatEnd
|
readonly property color actualColor: root.actualProgressColor
|
||||||
x: root.width
|
|
||||||
y: root.midY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
|
||||||
id: waveClip
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
readonly property real startX: snap(root.lineWidth / 2)
|
|
||||||
readonly property real aaBias: (0.25 / root.dpr)
|
|
||||||
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
|
||||||
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
|
|
||||||
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: mask
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
x: 0
|
|
||||||
width: waveClip.endX
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Shape {
|
|
||||||
id: waveShape
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: parent.width + 4 * root.wavelength
|
|
||||||
antialiasing: true
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
x: waveOffsetX
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: wavePath
|
|
||||||
strokeColor: root.fillColor
|
|
||||||
strokeWidth: snap(root.lineWidth)
|
|
||||||
capStyle: ShapePath.RoundCap
|
|
||||||
joinStyle: ShapePath.RoundJoin
|
|
||||||
fillColor: "transparent"
|
|
||||||
PathSvg {
|
|
||||||
id: waveSvg
|
|
||||||
path: ""
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: actualMask
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
x: waveClip.gapStartX
|
|
||||||
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
visible: (root.previewBehind || root.previewAhead) && width > 0
|
|
||||||
|
|
||||||
Shape {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: root.width + 4 * root.wavelength
|
|
||||||
antialiasing: true
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
x: waveOffsetX
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
strokeColor: root.actualProgressColor
|
|
||||||
strokeWidth: snap(root.lineWidth)
|
|
||||||
capStyle: ShapePath.RoundCap
|
|
||||||
joinStyle: ShapePath.RoundJoin
|
|
||||||
fillColor: "transparent"
|
|
||||||
PathSvg {
|
|
||||||
path: waveSvg.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: startCap
|
|
||||||
width: snap(root.lineWidth)
|
|
||||||
height: snap(root.lineWidth)
|
|
||||||
radius: width / 2
|
|
||||||
color: root.fillColor
|
|
||||||
x: waveClip.startX - width / 2
|
|
||||||
y: waveY(waveClip.startX) - height / 2
|
|
||||||
visible: waveClip.endX > waveClip.startX
|
|
||||||
z: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: endCap
|
|
||||||
width: snap(root.lineWidth)
|
|
||||||
height: snap(root.lineWidth)
|
|
||||||
radius: width / 2
|
|
||||||
color: root.fillColor
|
|
||||||
x: waveClip.endX - width / 2
|
|
||||||
y: waveY(waveClip.endX) - height / 2
|
|
||||||
visible: waveClip.endX > waveClip.startX
|
|
||||||
z: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: actualEndCap
|
|
||||||
width: snap(root.lineWidth)
|
|
||||||
height: snap(root.lineWidth)
|
|
||||||
radius: width / 2
|
|
||||||
color: root.actualProgressColor
|
|
||||||
x: waveClip.gapEndX - width / 2
|
|
||||||
y: waveY(waveClip.gapEndX) - height / 2
|
|
||||||
visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
|
|
||||||
z: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: actualMarker
|
|
||||||
width: 2
|
|
||||||
height: Math.max(root.lineWidth + 4, 10)
|
|
||||||
radius: width / 2
|
|
||||||
color: root.actualProgressColor
|
|
||||||
x: root.actualX - width / 2
|
|
||||||
y: root.midY - height / 2
|
|
||||||
visible: root.showActualPlaybackState
|
|
||||||
z: 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: playhead
|
|
||||||
width: 3.5
|
|
||||||
height: Math.max(root.lineWidth + 12, 16)
|
|
||||||
radius: width / 2
|
|
||||||
color: root.playheadColor
|
|
||||||
x: root.playX - width / 2
|
|
||||||
y: root.midY - height / 2
|
|
||||||
z: 3
|
|
||||||
}
|
|
||||||
|
|
||||||
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
|
|
||||||
function wrapMod(a, m) {
|
|
||||||
let r = a % m;
|
|
||||||
return r < 0 ? r + m : r;
|
|
||||||
}
|
|
||||||
function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
|
|
||||||
return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
|
||||||
|
|
||||||
FrameAnimation {
|
FrameAnimation {
|
||||||
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (root.isPlaying)
|
if (root.isPlaying)
|
||||||
root.phase += 0.03 * frameTime * 60;
|
root.phase += 0.03 * frameTime * 60;
|
||||||
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
|
|
||||||
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
|
|
||||||
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildStaticWave() {
|
|
||||||
const start = waveClip.startX - 2 * root.wavelength;
|
|
||||||
const end = width + 2 * root.wavelength;
|
|
||||||
if (end <= start) {
|
|
||||||
waveSvg.path = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kLocal = k;
|
|
||||||
const halfPeriod = root.wavelength / 2;
|
|
||||||
function y0(x) {
|
|
||||||
return root.midY + root.currentAmp * Math.sin(kLocal * x);
|
|
||||||
}
|
|
||||||
function dy0(x) {
|
|
||||||
return root.currentAmp * Math.cos(kLocal * x) * kLocal;
|
|
||||||
}
|
|
||||||
|
|
||||||
let x0 = start;
|
|
||||||
let d = `M ${x0} ${y0(x0)}`;
|
|
||||||
while (x0 < end) {
|
|
||||||
const x1 = Math.min(x0 + halfPeriod, end);
|
|
||||||
const dx = x1 - x0;
|
|
||||||
const yA = y0(x0), yB = y0(x1);
|
|
||||||
const dyA = dy0(x0), dyB = dy0(x1);
|
|
||||||
const c1x = x0 + dx / 3;
|
|
||||||
const c1y = yA + (dyA * dx) / 3;
|
|
||||||
const c2x = x1 - dx / 3;
|
|
||||||
const c2y = yB - (dyB * dx) / 3;
|
|
||||||
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`;
|
|
||||||
x0 = x1;
|
|
||||||
}
|
|
||||||
waveSvg.path = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
currentAmp = isPlaying ? amp : 0;
|
|
||||||
buildStaticWave();
|
|
||||||
}
|
|
||||||
onWidthChanged: {
|
|
||||||
flatEnd.x = width;
|
|
||||||
buildStaticWave();
|
|
||||||
}
|
|
||||||
onHeightChanged: buildStaticWave()
|
|
||||||
onCurrentAmpChanged: buildStaticWave()
|
|
||||||
onWavelengthChanged: {
|
|
||||||
k = (2 * Math.PI) / Math.max(1e-6, wavelength);
|
|
||||||
buildStaticWave();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ LANGUAGES = {
|
|||||||
"de": "de.json",
|
"de": "de.json",
|
||||||
"sv": "sv.json",
|
"sv": "sv.json",
|
||||||
"vi": "vi.json",
|
"vi": "vi.json",
|
||||||
"eo": "eo.json"
|
"eo": "eo.json",
|
||||||
|
"ko": "ko.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
def error(msg):
|
def error(msg):
|
||||||
|
|||||||
Reference in New Issue
Block a user