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

themes: incorporate theme registry, browser, dms URI scheme handling

This commit is contained in:
bbedward
2025-12-21 22:03:48 -05:00
parent 67ee74ac20
commit b4f83d09d4
28 changed files with 1924 additions and 58 deletions

View File

@@ -1,10 +1,10 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=DMS Application Picker Name=DMS
Comment=Select an application to open links and files Comment=Select an application to open links and files
Exec=dms open %u Exec=dms open %u
Icon=danklogo Icon=danklogo
Terminal=false Terminal=false
NoDisplay=true NoDisplay=true
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml; MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/dms;text/html;application/xhtml+xml;
Categories=Utility; Categories=Utility;

View File

@@ -131,6 +131,12 @@ func runOpen(target string) {
detectedRequestType = "url" detectedRequestType = "url"
} }
log.Infof("Detected HTTP(S) URL") log.Infof("Detected HTTP(S) URL")
} else if strings.HasPrefix(target, "dms://") {
// Handle DMS internal URLs (theme/plugin install, etc.)
if detectedRequestType == "" {
detectedRequestType = "url"
}
log.Infof("Detected DMS internal URL")
} else if _, err := os.Stat(target); err == nil { } else if _, err := os.Stat(target); err == nil {
// Handle local file paths directly (not file:// URIs) // Handle local file paths directly (not file:// URIs)
// Convert to absolute path // Convert to absolute path
@@ -177,7 +183,7 @@ func runOpen(target string) {
} }
method := "apppicker.open" method := "apppicker.open"
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) { if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "dms://")) {
method = "browser.open" method = "browser.open"
params["url"] = target params["url"] = target
} }

View File

@@ -18,6 +18,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins" serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
) )
@@ -37,6 +38,11 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
if strings.HasPrefix(req.Method, "themes.") {
serverThemes.HandleRequest(conn, req)
return
}
if strings.HasPrefix(req.Method, "loginctl.") { if strings.HasPrefix(req.Method, "loginctl.") {
if loginctlManager == nil { if loginctlManager == nil {
models.RespondError(conn, req.ID, "loginctl manager not initialized") models.RespondError(conn, req.ID, "loginctl manager not initialized")

View File

@@ -0,0 +1,27 @@
package themes
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn net.Conn, req models.Request) {
switch req.Method {
case "themes.list":
HandleList(conn, req)
case "themes.listInstalled":
HandleListInstalled(conn, req)
case "themes.install":
HandleInstall(conn, req)
case "themes.uninstall":
HandleUninstall(conn, req)
case "themes.update":
HandleUpdate(conn, req)
case "themes.search":
HandleSearch(conn, req)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}

View File

@@ -0,0 +1,52 @@
package themes
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
)
func HandleInstall(conn net.Conn, req models.Request) {
idOrName, ok := req.Params["name"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return
}
registry, err := themes.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
themeList, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
return
}
theme := themes.FindByIDOrName(idOrName, themeList)
if theme == nil {
models.RespondError(conn, req.ID, fmt.Sprintf("theme not found: %s", idOrName))
return
}
manager, err := themes.NewManager()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return
}
registryThemeDir := registry.GetThemeDir(theme.SourceDir)
if err := manager.Install(*theme, registryThemeDir); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to install theme: %v", err))
return
}
models.Respond(conn, req.ID, models.SuccessResult{
Success: true,
Message: fmt.Sprintf("theme installed: %s", theme.Name),
})
}

View File

@@ -0,0 +1,52 @@
package themes
import (
"fmt"
"net"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
)
func HandleList(conn net.Conn, req models.Request) {
registry, err := themes.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
themeList, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
return
}
manager, err := themes.NewManager()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return
}
result := make([]ThemeInfo, len(themeList))
for i, t := range themeList {
installed, _ := manager.IsInstalled(t)
result[i] = ThemeInfo{
ID: t.ID,
Name: t.Name,
Version: t.Version,
Author: t.Author,
Description: t.Description,
PreviewPath: t.PreviewPath,
SourceDir: t.SourceDir,
Installed: installed,
FirstParty: isFirstParty(t.Author),
}
}
models.Respond(conn, req.ID, result)
}
func isFirstParty(author string) bool {
return strings.EqualFold(author, "Avenge Media") || strings.EqualFold(author, "AvengeMedia")
}

View File

@@ -0,0 +1,82 @@
package themes
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
)
func HandleListInstalled(conn net.Conn, req models.Request) {
manager, err := themes.NewManager()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return
}
installedIDs, err := manager.ListInstalled()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list installed themes: %v", err))
return
}
registry, err := themes.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
allThemes, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
return
}
themeMap := make(map[string]themes.Theme)
for _, t := range allThemes {
themeMap[t.ID] = t
}
result := make([]ThemeInfo, 0, len(installedIDs))
for _, id := range installedIDs {
if theme, ok := themeMap[id]; ok {
hasUpdate := false
if hasUpdates, err := manager.HasUpdates(id, theme); err == nil {
hasUpdate = hasUpdates
}
result = append(result, ThemeInfo{
ID: theme.ID,
Name: theme.Name,
Version: theme.Version,
Author: theme.Author,
Description: theme.Description,
SourceDir: id,
FirstParty: isFirstParty(theme.Author),
HasUpdate: hasUpdate,
})
} else {
installed, err := manager.GetInstalledTheme(id)
if err != nil {
result = append(result, ThemeInfo{
ID: id,
Name: id,
SourceDir: id,
})
continue
}
result = append(result, ThemeInfo{
ID: installed.ID,
Name: installed.Name,
Version: installed.Version,
Author: installed.Author,
Description: installed.Description,
SourceDir: id,
FirstParty: isFirstParty(installed.Author),
})
}
}
models.Respond(conn, req.ID, result)
}

View File

@@ -0,0 +1,53 @@
package themes
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
)
func HandleSearch(conn net.Conn, req models.Request) {
query, ok := req.Params["query"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'query' parameter")
return
}
registry, err := themes.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
themeList, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
return
}
searchResults := themes.FuzzySearch(query, themeList)
manager, err := themes.NewManager()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return
}
result := make([]ThemeInfo, len(searchResults))
for i, t := range searchResults {
installed, _ := manager.IsInstalled(t)
result[i] = ThemeInfo{
ID: t.ID,
Name: t.Name,
Version: t.Version,
Author: t.Author,
Description: t.Description,
Installed: installed,
FirstParty: isFirstParty(t.Author),
}
}
models.Respond(conn, req.ID, result)
}

View File

@@ -0,0 +1,14 @@
package themes
type ThemeInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Author string `json:"author,omitempty"`
Description string `json:"description,omitempty"`
PreviewPath string `json:"previewPath,omitempty"`
SourceDir string `json:"sourceDir,omitempty"`
Installed bool `json:"installed,omitempty"`
FirstParty bool `json:"firstParty,omitempty"`
HasUpdate bool `json:"hasUpdate,omitempty"`
}

View File

@@ -0,0 +1,63 @@
package themes
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
)
func HandleUninstall(conn net.Conn, req models.Request) {
idOrName, ok := req.Params["name"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return
}
manager, err := themes.NewManager()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return
}
registry, err := themes.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
themeList, _ := registry.List()
theme := themes.FindByIDOrName(idOrName, themeList)
if theme != nil {
installed, err := manager.IsInstalled(*theme)
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if theme is installed: %v", err))
return
}
if !installed {
models.RespondError(conn, req.ID, fmt.Sprintf("theme not installed: %s", idOrName))
return
}
if err := manager.Uninstall(*theme); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to uninstall theme: %v", err))
return
}
models.Respond(conn, req.ID, models.SuccessResult{
Success: true,
Message: fmt.Sprintf("theme uninstalled: %s", theme.Name),
})
return
}
if err := manager.UninstallByID(idOrName); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("theme not found: %s", idOrName))
return
}
models.Respond(conn, req.ID, models.SuccessResult{
Success: true,
Message: fmt.Sprintf("theme uninstalled: %s", idOrName),
})
}

View File

@@ -0,0 +1,57 @@
package themes
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
)
func HandleUpdate(conn net.Conn, req models.Request) {
idOrName, ok := req.Params["name"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return
}
manager, err := themes.NewManager()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return
}
registry, err := themes.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
themeList, _ := registry.List()
theme := themes.FindByIDOrName(idOrName, themeList)
if theme == nil {
models.RespondError(conn, req.ID, fmt.Sprintf("theme not found in registry: %s", idOrName))
return
}
installed, err := manager.IsInstalled(*theme)
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if theme is installed: %v", err))
return
}
if !installed {
models.RespondError(conn, req.ID, fmt.Sprintf("theme not installed: %s", idOrName))
return
}
if err := manager.Update(*theme); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to update theme: %v", err))
return
}
models.Respond(conn, req.ID, models.SuccessResult{
Success: true,
Message: fmt.Sprintf("theme updated: %s", theme.Name),
})
}

View File

@@ -0,0 +1,244 @@
package themes
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/afero"
)
type Manager struct {
fs afero.Fs
themesDir string
}
func NewManager() (*Manager, error) {
return NewManagerWithFs(afero.NewOsFs())
}
func NewManagerWithFs(fs afero.Fs) (*Manager, error) {
themesDir := getThemesDir()
return &Manager{
fs: fs,
themesDir: themesDir,
}, nil
}
func getThemesDir() string {
configDir, err := os.UserConfigDir()
if err != nil {
log.Error("failed to get user config dir", "err", err)
return ""
}
return filepath.Join(configDir, "DankMaterialShell", "themes")
}
func (m *Manager) IsInstalled(theme Theme) (bool, error) {
path := m.getInstalledPath(theme.ID)
exists, err := afero.Exists(m.fs, path)
if err != nil {
return false, err
}
return exists, nil
}
func (m *Manager) getInstalledDir(themeID string) string {
return filepath.Join(m.themesDir, themeID)
}
func (m *Manager) getInstalledPath(themeID string) string {
return filepath.Join(m.getInstalledDir(themeID), "theme.json")
}
func (m *Manager) Install(theme Theme, registryThemeDir string) error {
themeDir := m.getInstalledDir(theme.ID)
exists, err := afero.DirExists(m.fs, themeDir)
if err != nil {
return fmt.Errorf("failed to check if theme exists: %w", err)
}
if exists {
return fmt.Errorf("theme already installed: %s", theme.Name)
}
if err := m.fs.MkdirAll(themeDir, 0755); err != nil {
return fmt.Errorf("failed to create theme directory: %w", err)
}
data, err := json.MarshalIndent(theme, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal theme: %w", err)
}
themePath := filepath.Join(themeDir, "theme.json")
if err := afero.WriteFile(m.fs, themePath, data, 0644); err != nil {
return fmt.Errorf("failed to write theme file: %w", err)
}
for _, preview := range []string{"preview-dark.svg", "preview-light.svg"} {
srcPath := filepath.Join(registryThemeDir, preview)
exists, _ := afero.Exists(m.fs, srcPath)
if !exists {
continue
}
data, err := afero.ReadFile(m.fs, srcPath)
if err != nil {
continue
}
dstPath := filepath.Join(themeDir, preview)
_ = afero.WriteFile(m.fs, dstPath, data, 0644)
}
return nil
}
func (m *Manager) InstallFromRegistry(registry *Registry, themeID string) error {
theme, err := registry.Get(themeID)
if err != nil {
return err
}
registryThemeDir := registry.GetThemeDir(theme.SourceDir)
return m.Install(*theme, registryThemeDir)
}
func (m *Manager) Update(theme Theme) error {
themePath := m.getInstalledPath(theme.ID)
exists, err := afero.Exists(m.fs, themePath)
if err != nil {
return fmt.Errorf("failed to check if theme exists: %w", err)
}
if !exists {
return fmt.Errorf("theme not installed: %s", theme.Name)
}
data, err := json.MarshalIndent(theme, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal theme: %w", err)
}
if err := afero.WriteFile(m.fs, themePath, data, 0644); err != nil {
return fmt.Errorf("failed to write theme file: %w", err)
}
return nil
}
func (m *Manager) Uninstall(theme Theme) error {
return m.UninstallByID(theme.ID)
}
func (m *Manager) UninstallByID(themeID string) error {
themeDir := m.getInstalledDir(themeID)
exists, err := afero.DirExists(m.fs, themeDir)
if err != nil {
return fmt.Errorf("failed to check if theme exists: %w", err)
}
if !exists {
return fmt.Errorf("theme not installed: %s", themeID)
}
if err := m.fs.RemoveAll(themeDir); err != nil {
return fmt.Errorf("failed to remove theme: %w", err)
}
return nil
}
func (m *Manager) ListInstalled() ([]string, error) {
exists, err := afero.DirExists(m.fs, m.themesDir)
if err != nil {
return nil, err
}
if !exists {
return []string{}, nil
}
entries, err := afero.ReadDir(m.fs, m.themesDir)
if err != nil {
return nil, fmt.Errorf("failed to read themes directory: %w", err)
}
var installed []string
for _, entry := range entries {
if !entry.IsDir() {
continue
}
themeID := entry.Name()
themePath := filepath.Join(m.themesDir, themeID, "theme.json")
if exists, _ := afero.Exists(m.fs, themePath); exists {
installed = append(installed, themeID)
}
}
return installed, nil
}
func (m *Manager) GetInstalledTheme(themeID string) (*Theme, error) {
themePath := m.getInstalledPath(themeID)
data, err := afero.ReadFile(m.fs, themePath)
if err != nil {
return nil, fmt.Errorf("failed to read theme file: %w", err)
}
var theme Theme
if err := json.Unmarshal(data, &theme); err != nil {
return nil, fmt.Errorf("failed to parse theme file: %w", err)
}
return &theme, nil
}
func (m *Manager) HasUpdates(themeID string, registryTheme Theme) (bool, error) {
installed, err := m.GetInstalledTheme(themeID)
if err != nil {
return false, err
}
return compareVersions(installed.Version, registryTheme.Version) < 0, nil
}
func compareVersions(installed, registry string) int {
installedParts := strings.Split(installed, ".")
registryParts := strings.Split(registry, ".")
maxLen := len(installedParts)
if len(registryParts) > maxLen {
maxLen = len(registryParts)
}
for i := 0; i < maxLen; i++ {
var installedNum, registryNum int
if i < len(installedParts) {
fmt.Sscanf(installedParts[i], "%d", &installedNum)
}
if i < len(registryParts) {
fmt.Sscanf(registryParts[i], "%d", &registryNum)
}
if installedNum < registryNum {
return -1
}
if installedNum > registryNum {
return 1
}
}
return 0
}
func (m *Manager) GetThemesDir() string {
return m.themesDir
}

View File

@@ -0,0 +1,236 @@
package themes
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/go-git/go-git/v6"
"github.com/spf13/afero"
)
const registryRepo = "https://github.com/AvengeMedia/dms-plugin-registry.git"
type ColorScheme struct {
Primary string `json:"primary"`
PrimaryText string `json:"primaryText"`
PrimaryContainer string `json:"primaryContainer"`
Secondary string `json:"secondary"`
Surface string `json:"surface"`
SurfaceText string `json:"surfaceText"`
SurfaceVariant string `json:"surfaceVariant"`
SurfaceVariantText string `json:"surfaceVariantText"`
SurfaceTint string `json:"surfaceTint"`
Background string `json:"background"`
BackgroundText string `json:"backgroundText"`
Outline string `json:"outline"`
SurfaceContainer string `json:"surfaceContainer"`
SurfaceContainerHigh string `json:"surfaceContainerHigh"`
Error string `json:"error"`
Warning string `json:"warning"`
Info string `json:"info"`
}
type Theme struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Author string `json:"author"`
Description string `json:"description"`
Dark ColorScheme `json:"dark"`
Light ColorScheme `json:"light"`
PreviewPath string `json:"-"`
SourceDir string `json:"sourceDir,omitempty"`
}
type GitClient interface {
PlainClone(path string, url string) error
Pull(path string) error
}
type realGitClient struct{}
func (g *realGitClient) PlainClone(path string, url string) error {
_, err := git.PlainClone(path, &git.CloneOptions{
URL: url,
Progress: os.Stdout,
})
return err
}
func (g *realGitClient) Pull(path string) error {
repo, err := git.PlainOpen(path)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
err = worktree.Pull(&git.PullOptions{})
if err != nil && err.Error() != "already up-to-date" {
return err
}
return nil
}
type Registry struct {
fs afero.Fs
cacheDir string
themes []Theme
git GitClient
}
func NewRegistry() (*Registry, error) {
return NewRegistryWithFs(afero.NewOsFs())
}
func NewRegistryWithFs(fs afero.Fs) (*Registry, error) {
cacheDir := getCacheDir()
return &Registry{
fs: fs,
cacheDir: cacheDir,
git: &realGitClient{},
}, nil
}
func getCacheDir() string {
return filepath.Join(os.TempDir(), "dankdots-plugin-registry")
}
func (r *Registry) Update() error {
exists, err := afero.DirExists(r.fs, r.cacheDir)
if err != nil {
return fmt.Errorf("failed to check cache directory: %w", err)
}
if !exists {
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
return fmt.Errorf("failed to clone registry: %w", err)
}
} else {
if err := r.git.Pull(r.cacheDir); err != nil {
if err := r.fs.RemoveAll(r.cacheDir); err != nil {
return fmt.Errorf("failed to remove corrupted registry: %w", err)
}
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
return fmt.Errorf("failed to re-clone registry: %w", err)
}
}
}
return r.loadThemes()
}
func (r *Registry) loadThemes() error {
themesDir := filepath.Join(r.cacheDir, "themes")
entries, err := afero.ReadDir(r.fs, themesDir)
if err != nil {
return fmt.Errorf("failed to read themes directory: %w", err)
}
r.themes = []Theme{}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
themeDir := filepath.Join(themesDir, entry.Name())
themeFile := filepath.Join(themeDir, "theme.json")
data, err := afero.ReadFile(r.fs, themeFile)
if err != nil {
continue
}
var theme Theme
if err := json.Unmarshal(data, &theme); err != nil {
continue
}
if theme.ID == "" {
theme.ID = entry.Name()
}
theme.SourceDir = entry.Name()
previewPath := filepath.Join(themeDir, "preview.svg")
if exists, _ := afero.Exists(r.fs, previewPath); exists {
theme.PreviewPath = previewPath
}
r.themes = append(r.themes, theme)
}
return nil
}
func (r *Registry) List() ([]Theme, error) {
if len(r.themes) == 0 {
if err := r.Update(); err != nil {
return nil, err
}
}
return SortByFirstParty(r.themes), nil
}
func (r *Registry) Search(query string) ([]Theme, error) {
allThemes, err := r.List()
if err != nil {
return nil, err
}
if query == "" {
return allThemes, nil
}
return SortByFirstParty(FuzzySearch(query, allThemes)), nil
}
func (r *Registry) Get(idOrName string) (*Theme, error) {
themes, err := r.List()
if err != nil {
return nil, err
}
for _, t := range themes {
if t.ID == idOrName {
return &t, nil
}
}
for _, t := range themes {
if t.Name == idOrName {
return &t, nil
}
}
return nil, fmt.Errorf("theme not found: %s", idOrName)
}
func (r *Registry) GetThemeSourcePath(themeID string) string {
return filepath.Join(r.cacheDir, "themes", themeID, "theme.json")
}
func (r *Registry) GetThemeDir(themeID string) string {
return filepath.Join(r.cacheDir, "themes", themeID)
}
func SortByFirstParty(themes []Theme) []Theme {
return themes
}

View File

@@ -0,0 +1,40 @@
package themes
import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
func FuzzySearch(query string, themes []Theme) []Theme {
if query == "" {
return themes
}
queryLower := strings.ToLower(query)
return utils.Filter(themes, func(t Theme) bool {
return fuzzyMatch(queryLower, strings.ToLower(t.Name)) ||
fuzzyMatch(queryLower, strings.ToLower(t.Description)) ||
fuzzyMatch(queryLower, strings.ToLower(t.Author))
})
}
func fuzzyMatch(query, text string) bool {
queryIdx := 0
for _, char := range text {
if queryIdx < len(query) && char == rune(query[queryIdx]) {
queryIdx++
}
}
return queryIdx == len(query)
}
func FindByIDOrName(idOrName string, themes []Theme) *Theme {
if t, found := utils.Find(themes, func(t Theme) bool { return t.ID == idOrName }); found {
return &t
}
if t, found := utils.Find(themes, func(t Theme) bool { return t.Name == idOrName }); found {
return &t
}
return nil
}

View File

@@ -63,6 +63,7 @@ Singleton {
property alias dankBarRightWidgetsModel: rightWidgetsModel property alias dankBarRightWidgetsModel: rightWidgetsModel
property string currentThemeName: "blue" property string currentThemeName: "blue"
property string currentThemeCategory: "generic"
property string customThemeFile: "" property string customThemeFile: ""
property string matugenScheme: "scheme-tonal-spot" property string matugenScheme: "scheme-tonal-spot"
property bool runUserMatugenTemplates: true property bool runUserMatugenTemplates: true
@@ -562,10 +563,12 @@ Singleton {
function applyStoredTheme() { function applyStoredTheme() {
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
Theme.currentThemeCategory = currentThemeCategory;
Theme.switchTheme(currentThemeName, false, false); Theme.switchTheme(currentThemeName, false, false);
} else { } else {
Qt.callLater(function () { Qt.callLater(function () {
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
Theme.currentThemeCategory = currentThemeCategory;
Theme.switchTheme(currentThemeName, false, false); Theme.switchTheme(currentThemeName, false, false);
} }
}); });

View File

@@ -474,24 +474,32 @@ Singleton {
if (themeName === dynamic) { if (themeName === dynamic) {
currentTheme = dynamic; currentTheme = dynamic;
if (currentThemeCategory !== "registry")
currentThemeCategory = dynamic; currentThemeCategory = dynamic;
} else if (themeName === custom) { } else if (themeName === custom) {
currentTheme = custom; currentTheme = custom;
if (currentThemeCategory !== "registry")
currentThemeCategory = custom; currentThemeCategory = custom;
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) { if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
loadCustomThemeFromFile(SettingsData.customThemeFile); loadCustomThemeFromFile(SettingsData.customThemeFile);
} }
} else if (themeName === "" && currentThemeCategory === "registry") {
// Registry category selected but no theme chosen yet
} else { } else {
currentTheme = themeName; currentTheme = themeName;
if (currentThemeCategory !== "registry") {
if (StockThemes.isCatppuccinVariant(themeName)) { if (StockThemes.isCatppuccinVariant(themeName)) {
currentThemeCategory = "catppuccin"; currentThemeCategory = "catppuccin";
} else { } else {
currentThemeCategory = "generic"; currentThemeCategory = "generic";
} }
} }
}
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode) if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode) {
SettingsData.set("currentThemeCategory", currentThemeCategory);
SettingsData.set("currentThemeName", currentTheme); SettingsData.set("currentThemeName", currentTheme);
}
if (!isGreeterMode) { if (!isGreeterMode) {
generateSystemThemesFromCurrentTheme(); generateSystemThemesFromCurrentTheme();

View File

@@ -7,6 +7,7 @@ function percentToUnit(v) {
var SPEC = { var SPEC = {
currentThemeName: { def: "blue", onChange: "applyStoredTheme" }, currentThemeName: { def: "blue", onChange: "applyStoredTheme" },
currentThemeCategory: { def: "generic" },
customThemeFile: { def: "" }, customThemeFile: { def: "" },
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" }, matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" }, runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },

View File

@@ -510,6 +510,22 @@ Item {
Connections { Connections {
target: DMSService target: DMSService
function onOpenUrlRequested(url) { function onOpenUrlRequested(url) {
if (url.startsWith("dms://theme/install/")) {
var themeId = url.replace("dms://theme/install/", "").split(/[?#]/)[0];
if (themeId) {
PopoutService.pendingThemeInstall = themeId;
PopoutService.openSettingsWithTab("theme");
}
return;
}
if (url.startsWith("dms://plugin/install/")) {
var pluginId = url.replace("dms://plugin/install/", "").split(/[?#]/)[0];
if (pluginId) {
PopoutService.pendingPluginInstall = pluginId;
PopoutService.openSettingsWithTab("plugins");
}
return;
}
browserPickerModal.url = url; browserPickerModal.url = url;
browserPickerModal.open(); browserPickerModal.open();
} }

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Modals.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -15,6 +16,7 @@ FloatingWindow {
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property bool isLoading: false property bool isLoading: false
property var parentModal: null property var parentModal: null
property bool pendingInstallHandled: false
function updateFilteredPlugins() { function updateFilteredPlugins() {
var filtered = []; var filtered = [];
@@ -61,16 +63,22 @@ FloatingWindow {
keyboardNavigationActive = false; keyboardNavigationActive = false;
} }
function installPlugin(pluginName) { function installPlugin(pluginName, enableAfterInstall) {
ToastService.showInfo("Installing plugin: " + pluginName); ToastService.showInfo(I18n.tr("Installing: %1", "installation progress").arg(pluginName));
DMSService.install(pluginName, response => { DMSService.install(pluginName, response => {
if (response.error) { if (response.error) {
ToastService.showError("Install failed: " + response.error); ToastService.showError(I18n.tr("Install failed: %1", "installation error").arg(response.error));
return; return;
} }
ToastService.showInfo("Plugin installed: " + pluginName); ToastService.showInfo(I18n.tr("Installed: %1", "installation success").arg(pluginName));
PluginService.scanPlugins(); PluginService.scanPlugins();
refreshPlugins(); refreshPlugins();
if (enableAfterInstall) {
Qt.callLater(() => {
PluginService.enablePlugin(pluginName);
hide();
});
}
}); });
} }
@@ -81,13 +89,27 @@ FloatingWindow {
DMSService.listInstalled(); DMSService.listInstalled();
} }
function checkPendingInstall() {
if (!PopoutService.pendingPluginInstall || pendingInstallHandled)
return;
pendingInstallHandled = true;
var pluginId = PopoutService.pendingPluginInstall;
PopoutService.pendingPluginInstall = "";
urlInstallConfirm.showWithOptions({
title: I18n.tr("Install Plugin", "plugin installation dialog title"),
message: I18n.tr("Install plugin '%1' from the DMS registry?", "plugin installation confirmation").arg(pluginId),
confirmText: I18n.tr("Install", "install action button"),
cancelText: I18n.tr("Cancel"),
onConfirm: () => installPlugin(pluginId, true),
onCancel: () => hide()
});
}
function show() { function show() {
if (parentModal) if (parentModal)
parentModal.shouldHaveFocus = false; parentModal.shouldHaveFocus = false;
visible = true; visible = true;
Qt.callLater(() => { Qt.callLater(() => browserSearchField.forceActiveFocus());
browserSearchField.forceActiveFocus();
});
} }
function hide() { function hide() {
@@ -102,7 +124,7 @@ FloatingWindow {
} }
objectName: "pluginBrowser" objectName: "pluginBrowser"
title: I18n.tr("Browse Plugins") title: I18n.tr("Browse Plugins", "plugin browser window title")
minimumSize: Qt.size(450, 400) minimumSize: Qt.size(450, 400)
implicitWidth: 600 implicitWidth: 600
implicitHeight: 650 implicitHeight: 650
@@ -111,9 +133,11 @@ FloatingWindow {
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
pendingInstallHandled = false;
refreshPlugins(); refreshPlugins();
Qt.callLater(() => { Qt.callLater(() => {
browserSearchField.forceActiveFocus(); browserSearchField.forceActiveFocus();
checkPendingInstall();
}); });
return; return;
} }
@@ -125,6 +149,10 @@ FloatingWindow {
isLoading = false; isLoading = false;
} }
ConfirmModal {
id: urlInstallConfirm
}
FocusScope { FocusScope {
id: browserKeyHandler id: browserKeyHandler
@@ -171,7 +199,7 @@ FloatingWindow {
StyledText { StyledText {
id: headerText id: headerText
text: I18n.tr("Browse Plugins") text: I18n.tr("Browse Plugins", "plugin browser header")
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -225,7 +253,7 @@ FloatingWindow {
anchors.right: parent.right anchors.right: parent.right
anchors.top: headerArea.bottom anchors.top: headerArea.bottom
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
text: I18n.tr("Install plugins from the DMS plugin registry") text: I18n.tr("Install plugins from the DMS plugin registry", "plugin browser description")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.outline color: Theme.outline
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -249,7 +277,7 @@ FloatingWindow {
showClearButton: true showClearButton: true
textColor: Theme.surfaceText textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search plugins...") placeholderText: I18n.tr("Search plugins...", "plugin search placeholder")
text: root.searchQuery text: root.searchQuery
focus: true focus: true
ignoreLeftRightKeys: true ignoreLeftRightKeys: true
@@ -293,7 +321,7 @@ FloatingWindow {
} }
StyledText { StyledText {
text: I18n.tr("Loading plugins...") text: I18n.tr("Loading...", "loading indicator")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -407,8 +435,8 @@ FloatingWindow {
StyledText { StyledText {
text: { text: {
const author = "by " + (modelData.author || "Unknown"); const author = I18n.tr("by %1", "author attribution").arg(modelData.author || I18n.tr("Unknown", "unknown author"));
const source = modelData.repo ? ` <a href="${modelData.repo}" style="text-decoration:none; color:${Theme.primary};">source</a>` : ""; const source = modelData.repo ? ` <a href="${modelData.repo}" style="text-decoration:none; color:${Theme.primary};">${I18n.tr("source", "source code link")}</a>` : "";
return author + source; return author + source;
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -458,7 +486,7 @@ FloatingWindow {
} }
StyledText { StyledText {
text: isInstalled ? "Installed" : "Install" text: isInstalled ? I18n.tr("Installed", "installed status") : I18n.tr("Install", "install action button")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: isInstalled ? Theme.surfaceText : Theme.surface color: isInstalled ? Theme.surfaceText : Theme.surface
@@ -474,7 +502,7 @@ FloatingWindow {
enabled: !isInstalled enabled: !isInstalled
onClicked: { onClicked: {
if (!isInstalled) if (!isInstalled)
root.installPlugin(modelData.name); root.installPlugin(modelData.name, false);
} }
} }
} }
@@ -521,7 +549,7 @@ FloatingWindow {
StyledText { StyledText {
anchors.centerIn: listArea anchors.centerIn: listArea
text: I18n.tr("No plugins found") text: I18n.tr("No plugins found", "empty plugin list")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
visible: !root.isLoading && root.filteredPlugins.length === 0 visible: !root.isLoading && root.filteredPlugins.length === 0

View File

@@ -387,6 +387,16 @@ FocusScope {
pluginBrowser.parentModal = pluginsTab.parentModal; pluginBrowser.parentModal = pluginsTab.parentModal;
if (DMSService.dmsAvailable && DMSService.apiVersion >= 8) if (DMSService.dmsAvailable && DMSService.apiVersion >= 8)
DMSService.listInstalled(); DMSService.listInstalled();
if (PopoutService.pendingPluginInstall)
Qt.callLater(() => pluginBrowser.show());
}
Connections {
target: PopoutService
function onPendingPluginInstallChanged() {
if (PopoutService.pendingPluginInstall)
pluginBrowser.show();
}
} }
PluginBrowser { PluginBrowser {

View File

@@ -0,0 +1,572 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
FloatingWindow {
id: root
property var allThemes: []
property string searchQuery: ""
property var filteredThemes: []
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property bool isLoading: false
property var parentModal: null
property bool pendingInstallHandled: false
property string pendingApplyThemeId: ""
function updateFilteredThemes() {
var filtered = [];
var query = searchQuery ? searchQuery.toLowerCase() : "";
for (var i = 0; i < allThemes.length; i++) {
var theme = allThemes[i];
if (query.length === 0) {
filtered.push(theme);
continue;
}
var name = theme.name ? theme.name.toLowerCase() : "";
var description = theme.description ? theme.description.toLowerCase() : "";
var author = theme.author ? theme.author.toLowerCase() : "";
if (name.indexOf(query) !== -1 || description.indexOf(query) !== -1 || author.indexOf(query) !== -1)
filtered.push(theme);
}
filteredThemes = filtered;
selectedIndex = -1;
keyboardNavigationActive = false;
}
function selectNext() {
if (filteredThemes.length === 0)
return;
keyboardNavigationActive = true;
selectedIndex = Math.min(selectedIndex + 1, filteredThemes.length - 1);
}
function selectPrevious() {
if (filteredThemes.length === 0)
return;
keyboardNavigationActive = true;
selectedIndex = Math.max(selectedIndex - 1, -1);
if (selectedIndex === -1)
keyboardNavigationActive = false;
}
function installTheme(themeId, themeName, applyAfterInstall) {
ToastService.showInfo(I18n.tr("Installing: %1", "installation progress").arg(themeName));
DMSService.installTheme(themeId, response => {
if (response.error) {
ToastService.showError(I18n.tr("Install failed: %1", "installation error").arg(response.error));
return;
}
ToastService.showInfo(I18n.tr("Installed: %1", "installation success").arg(themeName));
if (applyAfterInstall)
pendingApplyThemeId = themeId;
refreshThemes();
});
}
function applyInstalledTheme(themeId, installedThemes) {
for (var i = 0; i < installedThemes.length; i++) {
var theme = installedThemes[i];
if (theme.id === themeId) {
var sourceDir = theme.sourceDir || theme.id;
var themePath = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + sourceDir + "/theme.json";
SettingsData.set("customThemeFile", themePath);
Theme.switchThemeCategory("registry", "custom");
Theme.switchTheme("custom", true, true);
hide();
return;
}
}
}
function uninstallTheme(themeId, themeName) {
ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(themeName));
DMSService.uninstallTheme(themeId, response => {
if (response.error) {
ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error));
return;
}
ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(themeName));
refreshThemes();
});
}
function refreshThemes() {
isLoading = true;
DMSService.listThemes();
DMSService.listInstalledThemes();
}
function checkPendingInstall() {
if (!PopoutService.pendingThemeInstall || pendingInstallHandled)
return;
pendingInstallHandled = true;
var themeId = PopoutService.pendingThemeInstall;
PopoutService.pendingThemeInstall = "";
urlInstallConfirm.showWithOptions({
title: I18n.tr("Install Theme", "theme installation dialog title"),
message: I18n.tr("Install theme '%1' from the DMS registry?", "theme installation confirmation").arg(themeId),
confirmText: I18n.tr("Install", "install action button"),
cancelText: I18n.tr("Cancel"),
onConfirm: () => installTheme(themeId, themeId, true),
onCancel: () => hide()
});
}
function show() {
if (parentModal)
parentModal.shouldHaveFocus = false;
visible = true;
Qt.callLater(() => browserSearchField.forceActiveFocus());
}
function hide() {
visible = false;
if (!parentModal)
return;
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
Qt.callLater(() => {
if (parentModal.modalFocusScope)
parentModal.modalFocusScope.forceActiveFocus();
});
}
objectName: "themeBrowser"
title: I18n.tr("Browse Themes", "theme browser window title")
minimumSize: Qt.size(550, 450)
implicitWidth: 700
implicitHeight: 700
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
if (visible) {
pendingInstallHandled = false;
refreshThemes();
Qt.callLater(() => {
browserSearchField.forceActiveFocus();
checkPendingInstall();
});
return;
}
allThemes = [];
searchQuery = "";
filteredThemes = [];
selectedIndex = -1;
keyboardNavigationActive = false;
isLoading = false;
}
ConfirmModal {
id: urlInstallConfirm
}
Connections {
target: DMSService
function onThemesListReceived(themes) {
isLoading = false;
allThemes = themes;
updateFilteredThemes();
}
function onInstalledThemesReceived(themes) {
if (!pendingApplyThemeId)
return;
var themeId = pendingApplyThemeId;
pendingApplyThemeId = "";
applyInstalledTheme(themeId, themes);
}
}
FocusScope {
id: browserKeyHandler
anchors.fill: parent
focus: true
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Escape:
root.hide();
event.accepted = true;
return;
case Qt.Key_Down:
root.selectNext();
event.accepted = true;
return;
case Qt.Key_Up:
root.selectPrevious();
event.accepted = true;
return;
}
}
Item {
id: browserContent
anchors.fill: parent
anchors.margins: Theme.spacingL
Item {
id: headerArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: Math.max(headerIcon.height, headerText.height, refreshButton.height, closeButton.height)
DankIcon {
id: headerIcon
name: "palette"
size: Theme.iconSize
color: Theme.primary
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: headerText
text: I18n.tr("Browse Themes", "theme browser header")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.left: headerIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankActionButton {
id: refreshButton
iconName: "refresh"
iconSize: 18
iconColor: Theme.primary
visible: !root.isLoading
onClicked: root.refreshThemes()
}
DankActionButton {
id: closeButton
iconName: "close"
iconSize: Theme.iconSize - 2
iconColor: Theme.outline
onClicked: root.hide()
}
}
}
StyledText {
id: descriptionText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerArea.bottom
anchors.topMargin: Theme.spacingM
text: I18n.tr("Install color themes from the DMS theme registry", "theme browser description")
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
wrapMode: Text.WordWrap
}
DankTextField {
id: browserSearchField
anchors.left: parent.left
anchors.right: parent.right
anchors.top: descriptionText.bottom
anchors.topMargin: Theme.spacingM
height: 48
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search themes...", "theme search placeholder")
text: root.searchQuery
focus: true
ignoreLeftRightKeys: true
keyForwardTargets: [browserKeyHandler]
onTextEdited: {
root.searchQuery = text;
root.updateFilteredThemes();
}
}
Item {
id: listArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: browserSearchField.bottom
anchors.topMargin: Theme.spacingM
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
Item {
anchors.fill: parent
visible: root.isLoading
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: "sync"
size: 48
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
RotationAnimator on rotation {
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
running: root.isLoading
}
}
StyledText {
text: I18n.tr("Loading...", "loading indicator")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
DankListView {
id: themeBrowserList
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
spacing: Theme.spacingS
model: ScriptModel {
values: root.filteredThemes
}
clip: true
visible: !root.isLoading
ScrollBar.vertical: DankScrollbar {
id: browserScrollbar
}
delegate: Rectangle {
width: themeBrowserList.width
height: hasPreview ? 140 : themeDelegateContent.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
property bool isInstalled: modelData.installed || false
property bool isFirstParty: modelData.firstParty || false
property string previewPath: "/tmp/dankdots-plugin-registry/themes/" + (modelData.sourceDir || modelData.id) + "/preview-" + (Theme.isLightMode ? "light" : "dark") + ".svg"
property bool hasPreview: previewImage.status === Image.Ready
color: isSelected ? Theme.primarySelected : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: isSelected ? 2 : 1
Row {
id: themeDelegateContent
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Rectangle {
width: hasPreview ? 180 : 0
height: parent.height
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
visible: hasPreview
Image {
id: previewImage
anchors.fill: parent
anchors.margins: 2
source: "file://" + previewPath
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
}
}
DankIcon {
name: "palette"
size: 48
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: !hasPreview
}
Column {
width: parent.width - (hasPreview ? 180 : 48) - Theme.spacingM - installButton.width - Theme.spacingM
spacing: 6
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingXS
width: parent.width
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
height: 18
width: versionText.implicitWidth + Theme.spacingS
radius: 9
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15)
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: versionText
anchors.centerIn: parent
text: modelData.version || "1.0.0"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
}
}
Rectangle {
height: 18
width: firstPartyText.implicitWidth + Theme.spacingS
radius: 9
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4)
border.width: 1
visible: isFirstParty
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: firstPartyText
anchors.centerIn: parent
text: I18n.tr("official")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
}
}
StyledText {
text: I18n.tr("by %1", "author attribution").arg(modelData.author || I18n.tr("Unknown", "unknown author"))
font.pixelSize: Theme.fontSizeMedium
color: Theme.outline
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.description || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
visible: modelData.description && modelData.description.length > 0
}
}
Rectangle {
id: installButton
width: 90
height: 36
radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter
color: isInstalled ? (uninstallMouseArea.containsMouse ? Theme.error : Theme.surfaceVariant) : Theme.primary
opacity: installMouseArea.containsMouse || uninstallMouseArea.containsMouse ? 0.9 : 1
border.width: isInstalled ? 1 : 0
border.color: isInstalled ? (uninstallMouseArea.containsMouse ? Theme.error : Theme.outline) : "transparent"
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: isInstalled ? (uninstallMouseArea.containsMouse ? "delete" : "check") : "download"
size: 16
color: isInstalled ? (uninstallMouseArea.containsMouse ? "white" : Theme.surfaceText) : Theme.surface
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!isInstalled)
return I18n.tr("Install", "install action button");
if (uninstallMouseArea.containsMouse)
return I18n.tr("Uninstall", "uninstall action button");
return I18n.tr("Installed", "installed status");
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: isInstalled ? (uninstallMouseArea.containsMouse ? "white" : Theme.surfaceText) : Theme.surface
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: installMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
visible: !isInstalled
onClicked: root.installTheme(modelData.id, modelData.name, false)
}
MouseArea {
id: uninstallMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
visible: isInstalled
onClicked: root.uninstallTheme(modelData.id, modelData.name)
}
}
}
}
}
StyledText {
anchors.centerIn: listArea
text: I18n.tr("No themes found", "empty theme list")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: !root.isLoading && root.filteredThemes.length === 0
}
}
}
}
}

View File

@@ -12,9 +12,29 @@ Item {
property var cachedIconThemes: SettingsData.availableIconThemes property var cachedIconThemes: SettingsData.availableIconThemes
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label) property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
property var installedRegistryThemes: []
Component.onCompleted: { Component.onCompleted: {
SettingsData.detectAvailableIconThemes(); SettingsData.detectAvailableIconThemes();
if (DMSService.dmsAvailable)
DMSService.listInstalledThemes();
if (PopoutService.pendingThemeInstall)
Qt.callLater(() => themeBrowser.show());
}
Connections {
target: DMSService
function onInstalledThemesReceived(themes) {
themeColorsTab.installedRegistryThemes = themes;
}
}
Connections {
target: PopoutService
function onPendingThemeInstallChanged() {
if (PopoutService.pendingThemeInstall)
themeBrowser.show();
}
} }
DankFlickable { DankFlickable {
@@ -41,12 +61,24 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText { StyledText {
property string registryThemeName: {
if (Theme.currentThemeCategory !== "registry")
return "";
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json"))
return t.name;
}
return "";
}
text: { text: {
if (Theme.currentTheme === Theme.dynamic) if (Theme.currentTheme === Theme.dynamic)
return "Current Theme: Dynamic"; return I18n.tr("Current Theme: %1", "current theme label").arg(I18n.tr("Dynamic", "dynamic theme name"));
if (Theme.currentThemeCategory === "registry" && registryThemeName)
return I18n.tr("Current Theme: %1", "current theme label").arg(registryThemeName);
if (Theme.currentThemeCategory === "catppuccin") if (Theme.currentThemeCategory === "catppuccin")
return "Current Theme: Catppuccin " + Theme.getThemeColors(Theme.currentThemeName).name; return I18n.tr("Current Theme: %1", "current theme label").arg("Catppuccin " + Theme.getThemeColors(Theme.currentThemeName).name);
return "Current Theme: " + Theme.getThemeColors(Theme.currentThemeName).name; return I18n.tr("Current Theme: %1", "current theme label").arg(Theme.getThemeColors(Theme.currentThemeName).name);
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
@@ -57,12 +89,14 @@ Item {
StyledText { StyledText {
text: { text: {
if (Theme.currentTheme === Theme.dynamic) if (Theme.currentTheme === Theme.dynamic)
return "Material colors generated from wallpaper"; return I18n.tr("Material colors generated from wallpaper", "dynamic theme description");
if (Theme.currentThemeCategory === "registry")
return I18n.tr("Color theme from DMS registry", "registry theme description");
if (Theme.currentThemeCategory === "catppuccin") if (Theme.currentThemeCategory === "catppuccin")
return "Soothing pastel theme based on Catppuccin"; return I18n.tr("Soothing pastel theme based on Catppuccin", "catppuccin theme description");
if (Theme.currentTheme === Theme.custom) if (Theme.currentTheme === Theme.custom)
return "Custom theme loaded from JSON file"; return I18n.tr("Custom theme loaded from JSON file", "custom theme description");
return "Material Design inspired color themes"; return I18n.tr("Material Design inspired color themes", "generic theme description");
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -78,7 +112,11 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
DankButtonGroup { DankButtonGroup {
id: themeCategoryGroup
property bool isRegistryTheme: Theme.currentThemeCategory === "registry"
property int currentThemeIndex: { property int currentThemeIndex: {
if (isRegistryTheme)
return 4;
if (Theme.currentTheme === Theme.dynamic) if (Theme.currentTheme === Theme.dynamic)
return 2; return 2;
if (Theme.currentThemeName === "custom") if (Theme.currentThemeName === "custom")
@@ -89,7 +127,7 @@ Item {
} }
property int pendingThemeIndex: -1 property int pendingThemeIndex: -1
model: ["Generic", "Catppuccin", "Auto", "Custom"] model: DMSService.dmsAvailable ? ["Generic", "Catppuccin", "Auto", "Custom", "Registry"] : ["Generic", "Catppuccin", "Auto", "Custom"]
currentIndex: currentThemeIndex currentIndex: currentThemeIndex
selectionMode: "single" selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -110,15 +148,17 @@ Item {
break; break;
case 2: case 2:
if (ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "matugen_missing")
ToastService.showError("matugen not found - install matugen package for dynamic theming"); ToastService.showError(I18n.tr("matugen not found - install matugen package for dynamic theming", "matugen error"));
else if (ToastService.wallpaperErrorStatus === "error") else if (ToastService.wallpaperErrorStatus === "error")
ToastService.showError("Wallpaper processing failed - check wallpaper path"); ToastService.showError(I18n.tr("Wallpaper processing failed - check wallpaper path", "wallpaper error"));
else else
Theme.switchTheme(Theme.dynamic, true, true); Theme.switchThemeCategory("dynamic", Theme.dynamic);
break; break;
case 3: case 3:
if (Theme.currentThemeName !== "custom") Theme.switchThemeCategory("custom", "custom");
Theme.switchTheme("custom", true, true); break;
case 4:
Theme.switchThemeCategory("registry", "");
break; break;
} }
pendingThemeIndex = -1; pendingThemeIndex = -1;
@@ -388,7 +428,7 @@ Item {
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
visible: Theme.currentTheme === Theme.dynamic visible: Theme.currentTheme === Theme.dynamic && Theme.currentThemeCategory !== "registry"
Row { Row {
width: parent.width width: parent.width
@@ -450,12 +490,12 @@ Item {
StyledText { StyledText {
text: { text: {
if (ToastService.wallpaperErrorStatus === "error") if (ToastService.wallpaperErrorStatus === "error")
return "Wallpaper Error"; return I18n.tr("Wallpaper Error", "wallpaper error status");
if (ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "Matugen Missing"; return I18n.tr("Matugen Missing", "matugen not found status");
if (Theme.wallpaperPath) if (Theme.wallpaperPath)
return Theme.wallpaperPath.split('/').pop(); return Theme.wallpaperPath.split('/').pop();
return "No wallpaper selected"; return I18n.tr("No wallpaper selected", "no wallpaper status");
} }
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
@@ -467,12 +507,12 @@ Item {
StyledText { StyledText {
text: { text: {
if (ToastService.wallpaperErrorStatus === "error") if (ToastService.wallpaperErrorStatus === "error")
return "Wallpaper processing failed"; return I18n.tr("Wallpaper processing failed", "wallpaper processing error");
if (ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "Install matugen package for dynamic theming"; return I18n.tr("Install matugen package for dynamic theming", "matugen installation hint");
if (Theme.wallpaperPath) if (Theme.wallpaperPath)
return Theme.wallpaperPath; return Theme.wallpaperPath;
return "Dynamic colors from wallpaper"; return I18n.tr("Dynamic colors from wallpaper", "dynamic colors description");
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceVariantText color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceVariantText
@@ -520,7 +560,7 @@ Item {
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
visible: Theme.currentThemeName === "custom" visible: Theme.currentThemeName === "custom" && Theme.currentThemeCategory !== "registry"
Row { Row {
width: parent.width width: parent.width
@@ -541,7 +581,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
text: SettingsData.customThemeFile ? SettingsData.customThemeFile.split('/').pop() : "No custom theme file" text: SettingsData.customThemeFile ? SettingsData.customThemeFile.split('/').pop() : I18n.tr("No custom theme file", "no custom theme file status")
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
elide: Text.ElideMiddle elide: Text.ElideMiddle
@@ -550,7 +590,7 @@ Item {
} }
StyledText { StyledText {
text: SettingsData.customThemeFile || "Click to select a custom theme JSON file" text: SettingsData.customThemeFile || I18n.tr("Click to select a custom theme JSON file", "custom theme file hint")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideMiddle elide: Text.ElideMiddle
@@ -560,6 +600,174 @@ Item {
} }
} }
} }
Column {
id: registrySection
width: parent.width
spacing: Theme.spacingM
visible: Theme.currentThemeCategory === "registry"
Grid {
columns: 3
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: themeColorsTab.installedRegistryThemes.length > 0
Repeater {
model: themeColorsTab.installedRegistryThemes
Rectangle {
id: themeCard
property bool isActive: Theme.currentThemeCategory === "registry" && Theme.currentThemeName === "custom" && SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((modelData.sourceDir || modelData.id) + "/theme.json")
property string previewPath: Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + (modelData.sourceDir || modelData.id) + "/preview-" + (Theme.isLightMode ? "light" : "dark") + ".svg"
width: 140
height: 100
radius: Theme.cornerRadius
color: Theme.surfaceVariant
border.color: isActive ? Theme.primary : Theme.outline
border.width: isActive ? 2 : 1
scale: isActive ? 1.03 : 1
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Image {
id: previewImage
anchors.fill: parent
anchors.margins: 2
source: "file://" + themeCard.previewPath
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
}
DankIcon {
anchors.centerIn: parent
name: "palette"
size: 32
color: Theme.primary
visible: previewImage.status === Image.Error || previewImage.status === Image.Null
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 24
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.6)
StyledText {
anchors.centerIn: parent
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
color: "white"
font.weight: Font.Medium
elide: Text.ElideRight
width: parent.width - Theme.spacingS * 2
horizontalAlignment: Text.AlignHCenter
}
}
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 4
width: 20
height: 20
radius: 10
color: Theme.primary
visible: themeCard.isActive
DankIcon {
anchors.centerIn: parent
name: "check"
size: 14
color: Theme.surface
}
}
MouseArea {
id: cardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const themesDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes";
const themePath = themesDir + "/" + (modelData.sourceDir || modelData.id) + "/theme.json";
SettingsData.set("customThemeFile", themePath);
Theme.switchTheme("custom", true, true);
}
}
Rectangle {
id: deleteButton
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 4
width: 24
height: 24
radius: 12
color: deleteMouseArea.containsMouse ? Theme.error : Qt.rgba(0, 0, 0, 0.6)
opacity: cardMouseArea.containsMouse || deleteMouseArea.containsMouse ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
DankIcon {
anchors.centerIn: parent
name: "close"
size: 14
color: "white"
}
MouseArea {
id: deleteMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(modelData.name));
DMSService.uninstallTheme(modelData.id, response => {
if (response.error) {
ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error));
return;
}
ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(modelData.name));
DMSService.listInstalledThemes();
});
}
}
}
}
}
}
StyledText {
text: I18n.tr("No themes installed. Browse themes to install from the registry.", "no registry themes installed hint")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
visible: themeColorsTab.installedRegistryThemes.length === 0
horizontalAlignment: Text.AlignHCenter
}
DankButton {
text: I18n.tr("Browse Themes", "browse themes button")
iconName: "store"
anchors.horizontalCenter: parent.horizontalCenter
onClicked: themeBrowser.show()
}
}
} }
} }
@@ -1086,4 +1294,8 @@ Item {
} }
} }
} }
ThemeBrowser {
id: themeBrowser
}
} }

View File

@@ -16,6 +16,8 @@ Singleton {
readonly property int expectedApiVersion: 1 readonly property int expectedApiVersion: 1
property var availablePlugins: [] property var availablePlugins: []
property var installedPlugins: [] property var installedPlugins: []
property var availableThemes: []
property var installedThemes: []
property bool isConnected: false property bool isConnected: false
property bool isConnecting: false property bool isConnecting: false
property bool subscribeConnected: false property bool subscribeConnected: false
@@ -33,6 +35,9 @@ Singleton {
signal pluginsListReceived(var plugins) signal pluginsListReceived(var plugins)
signal installedPluginsReceived(var plugins) signal installedPluginsReceived(var plugins)
signal searchResultsReceived(var plugins) signal searchResultsReceived(var plugins)
signal themesListReceived(var themes)
signal installedThemesReceived(var themes)
signal themeSearchResultsReceived(var themes)
signal operationSuccess(string message) signal operationSuccess(string message)
signal operationError(string error) signal operationError(string error)
signal connectionStateChanged signal connectionStateChanged
@@ -514,6 +519,82 @@ Singleton {
}); });
} }
function listThemes(callback) {
sendRequest("themes.list", null, response => {
if (response.result) {
availableThemes = response.result;
themesListReceived(response.result);
}
if (callback) {
callback(response);
}
});
}
function listInstalledThemes(callback) {
sendRequest("themes.listInstalled", null, response => {
if (response.result) {
installedThemes = response.result;
installedThemesReceived(response.result);
}
if (callback) {
callback(response);
}
});
}
function searchThemes(query, callback) {
sendRequest("themes.search", {
"query": query
}, response => {
if (response.result) {
themeSearchResultsReceived(response.result);
}
if (callback) {
callback(response);
}
});
}
function installTheme(themeName, callback) {
sendRequest("themes.install", {
"name": themeName
}, response => {
if (callback) {
callback(response);
}
if (!response.error) {
listInstalledThemes();
}
});
}
function uninstallTheme(themeName, callback) {
sendRequest("themes.uninstall", {
"name": themeName
}, response => {
if (callback) {
callback(response);
}
if (!response.error) {
listInstalledThemes();
}
});
}
function updateTheme(themeName, callback) {
sendRequest("themes.update", {
"name": themeName
}, response => {
if (callback) {
callback(response);
}
if (!response.error) {
listInstalledThemes();
}
});
}
function lockSession(callback) { function lockSession(callback) {
sendRequest("loginctl.lock", null, callback); sendRequest("loginctl.lock", null, callback);
} }

View File

@@ -32,6 +32,9 @@ Singleton {
property var notepadSlideouts: [] property var notepadSlideouts: []
property string pendingThemeInstall: ""
property string pendingPluginInstall: ""
function setPosition(popout, x, y, width, section, screen) { function setPosition(popout, x, y, width, section, screen) {
if (popout && popout.setTriggerPosition && arguments.length >= 6) { if (popout && popout.setTriggerPosition && arguments.length >= 6) {
popout.setTriggerPosition(x, y, width, section, screen); popout.setTriggerPosition(x, y, width, section, screen);

View File

@@ -9,8 +9,8 @@ tab_bar_margin_color {{colors.background.default.hex}}
tab_bar_background {{colors.background.default.hex}} tab_bar_background {{colors.background.default.hex}}
active_tab_foreground {{colors.on_primary_container.default.hex}} active_tab_foreground {{colors.on_primary.default.hex}}
active_tab_background {{colors.primary_container.default.hex}} active_tab_background {{colors.primary.default.hex}}
active_tab_font_style bold active_tab_font_style bold
inactive_tab_foreground {{colors.on_surface_variant.default.hex}} inactive_tab_foreground {{colors.on_surface_variant.default.hex}}

View File

@@ -58,8 +58,8 @@
// //
// Lists (files, search results, etc.) // Lists (files, search results, etc.)
// //
"list.activeSelectionBackground": "{{colors.primary_container.dark.hex}}", "list.activeSelectionBackground": "{{colors.primary.dark.hex}}",
"list.activeSelectionForeground": "{{colors.on_primary_container.dark.hex}}", "list.activeSelectionForeground": "{{colors.on_primary.dark.hex}}",
"list.inactiveSelectionBackground": "{{colors.surface_container.dark.hex}}", "list.inactiveSelectionBackground": "{{colors.surface_container.dark.hex}}",
"list.hoverBackground": "{{colors.surface_container.dark.hex}}", "list.hoverBackground": "{{colors.surface_container.dark.hex}}",
"list.hoverForeground": "{{colors.on_surface.dark.hex}}", "list.hoverForeground": "{{colors.on_surface.dark.hex}}",

View File

@@ -40,8 +40,8 @@
"tab.activeForeground": "{{colors.on_surface.default.hex}}", "tab.activeForeground": "{{colors.on_surface.default.hex}}",
"tab.inactiveForeground": "{{colors.outline.default.hex}}", "tab.inactiveForeground": "{{colors.outline.default.hex}}",
"tab.activeBorderTop": "{{colors.primary.default.hex}}", "tab.activeBorderTop": "{{colors.primary.default.hex}}",
"list.activeSelectionBackground": "{{colors.primary_container.default.hex}}", "list.activeSelectionBackground": "{{colors.primary.default.hex}}",
"list.activeSelectionForeground": "{{colors.on_primary_container.default.hex}}", "list.activeSelectionForeground": "{{colors.on_primary.default.hex}}",
"list.inactiveSelectionBackground": "{{colors.surface_container.default.hex}}", "list.inactiveSelectionBackground": "{{colors.surface_container.default.hex}}",
"list.hoverBackground": "{{colors.surface_container.default.hex}}", "list.hoverBackground": "{{colors.surface_container.default.hex}}",
"list.hoverForeground": "{{colors.on_surface.default.hex}}", "list.hoverForeground": "{{colors.on_surface.default.hex}}",

View File

@@ -69,8 +69,8 @@
"sideBarTitle.foreground": "{{colors.on_surface.light.hex}}", "sideBarTitle.foreground": "{{colors.on_surface.light.hex}}",
"sideBarSectionHeader.background": "{{colors.surface_container_low.light.hex}}", "sideBarSectionHeader.background": "{{colors.surface_container_low.light.hex}}",
"sideBarSectionHeader.foreground": "{{colors.on_surface.light.hex}}", "sideBarSectionHeader.foreground": "{{colors.on_surface.light.hex}}",
"list.activeSelectionBackground": "{{colors.primary_container.light.hex}}", "list.activeSelectionBackground": "{{colors.primary.light.hex}}",
"list.activeSelectionForeground": "{{colors.on_primary_container.light.hex}}", "list.activeSelectionForeground": "{{colors.on_primary.light.hex}}",
"list.inactiveSelectionBackground": "{{colors.surface_container.light.hex}}", "list.inactiveSelectionBackground": "{{colors.surface_container.light.hex}}",
"list.inactiveSelectionForeground": "{{colors.on_surface.light.hex}}", "list.inactiveSelectionForeground": "{{colors.on_surface.light.hex}}",
"list.hoverBackground": "{{colors.surface_container.light.hex}}", "list.hoverBackground": "{{colors.surface_container.light.hex}}",