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

Compare commits

...

5 Commits

Author SHA1 Message Date
xxyangyoulin
1194f3ffb8 media: add scroll wheel behavior configuration (#1160) 2025-12-26 14:43:47 -05:00
bbedward
5ac81e6dd6 dankbar: dont apply exclusive zone to popup positioning 2025-12-26 14:38:34 -05:00
Lucas
987856a1de nix: update flake inputs (#1161) 2025-12-26 14:15:22 -05:00
bbedward
ef52ce0990 themes: support for variants 2025-12-26 14:00:14 -05:00
bbedward
06b14a5869 dankinstall: fix plasma session collision 2025-12-26 13:06:21 -05:00
17 changed files with 563 additions and 378 deletions

View File

@@ -199,6 +199,16 @@ func runShellInteractive(session bool) {
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -433,6 +443,16 @@ func runShellDaemon(session bool) {
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
}
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
log.Fatalf("Error opening /dev/null: %v", err)

View File

@@ -550,10 +550,7 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
terminalCmd = "ghostty"
}
content := fmt.Sprintf(`QT_QPA_PLATFORM=wayland
ELECTRON_OZONE_PLATFORM_HINT=auto
QT_QPA_PLATFORMTHEME=gtk3
QT_QPA_PLATFORMTHEME_QT6=gtk3
content := fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto
TERMINAL=%s
`, terminalCmd)
@@ -567,12 +564,6 @@ TERMINAL=%s
}
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
cmd := exec.CommandContext(ctx, "systemctl", "--user", "enable", "--now", "dms")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to enable dms service: %w", err)
}
b.log("Enabled dms systemd user service")
switch wm {
case deps.WindowManagerNiri:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {

View File

@@ -31,7 +31,7 @@ func HandleList(conn net.Conn, req models.Request) {
result := make([]ThemeInfo, len(themeList))
for i, t := range themeList {
installed, _ := manager.IsInstalled(t)
result[i] = ThemeInfo{
info := ThemeInfo{
ID: t.ID,
Name: t.Name,
Version: t.Version,
@@ -42,6 +42,17 @@ func HandleList(conn net.Conn, req models.Request) {
Installed: installed,
FirstParty: isFirstParty(t.Author),
}
if t.Variants != nil && len(t.Variants.Options) > 0 {
info.HasVariants = true
info.Variants = &VariantsInfo{
Default: t.Variants.Default,
Options: make([]VariantInfo, len(t.Variants.Options)),
}
for j, v := range t.Variants.Options {
info.Variants.Options[j] = VariantInfo{ID: v.ID, Name: v.Name}
}
}
result[i] = info
}
models.Respond(conn, req.ID, result)

View File

@@ -8,6 +8,20 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
)
func addVariantsInfo(info *ThemeInfo, variants *themes.ThemeVariants) {
if variants == nil || len(variants.Options) == 0 {
return
}
info.HasVariants = true
info.Variants = &VariantsInfo{
Default: variants.Default,
Options: make([]VariantInfo, len(variants.Options)),
}
for i, v := range variants.Options {
info.Variants.Options[i] = VariantInfo{ID: v.ID, Name: v.Name}
}
}
func HandleListInstalled(conn net.Conn, req models.Request) {
manager, err := themes.NewManager()
if err != nil {
@@ -46,7 +60,7 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
hasUpdate = hasUpdates
}
result = append(result, ThemeInfo{
info := ThemeInfo{
ID: theme.ID,
Name: theme.Name,
Version: theme.Version,
@@ -55,7 +69,9 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
SourceDir: id,
FirstParty: isFirstParty(theme.Author),
HasUpdate: hasUpdate,
})
}
addVariantsInfo(&info, theme.Variants)
result = append(result, info)
} else {
installed, err := manager.GetInstalledTheme(id)
if err != nil {
@@ -66,7 +82,7 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
})
continue
}
result = append(result, ThemeInfo{
info := ThemeInfo{
ID: installed.ID,
Name: installed.Name,
Version: installed.Version,
@@ -74,7 +90,9 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
Description: installed.Description,
SourceDir: id,
FirstParty: isFirstParty(installed.Author),
})
}
addVariantsInfo(&info, installed.Variants)
result = append(result, info)
}
}

View File

@@ -1,14 +1,26 @@
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"`
type VariantInfo struct {
ID string `json:"id"`
Name string `json:"name"`
}
type VariantsInfo struct {
Default string `json:"default,omitempty"`
Options []VariantInfo `json:"options,omitempty"`
}
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"`
HasVariants bool `json:"hasVariants,omitempty"`
Variants *VariantsInfo `json:"variants,omitempty"`
}

View File

@@ -80,21 +80,35 @@ func (m *Manager) Install(theme Theme, registryThemeDir string) error {
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 {
m.copyPreviewFiles(registryThemeDir, themeDir, theme)
return nil
}
func (m *Manager) copyPreviewFiles(srcDir, dstDir string, theme Theme) {
previews := []string{"preview-dark.svg", "preview-light.svg"}
if theme.Variants != nil {
for _, v := range theme.Variants.Options {
previews = append(previews,
fmt.Sprintf("preview-%s.svg", v.ID),
fmt.Sprintf("preview-%s-dark.svg", v.ID),
fmt.Sprintf("preview-%s-light.svg", v.ID),
)
}
}
for _, preview := range previews {
srcPath := filepath.Join(srcDir, preview)
if exists, _ := afero.Exists(m.fs, srcPath); !exists {
continue
}
data, err := afero.ReadFile(m.fs, srcPath)
if err != nil {
continue
}
dstPath := filepath.Join(themeDir, preview)
dstPath := filepath.Join(dstDir, preview)
_ = afero.WriteFile(m.fs, dstPath, data, 0644)
}
return nil
}
func (m *Manager) InstallFromRegistry(registry *Registry, themeID string) error {

View File

@@ -13,35 +13,49 @@ import (
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"`
Primary string `json:"primary,omitempty"`
PrimaryText string `json:"primaryText,omitempty"`
PrimaryContainer string `json:"primaryContainer,omitempty"`
Secondary string `json:"secondary,omitempty"`
Surface string `json:"surface,omitempty"`
SurfaceText string `json:"surfaceText,omitempty"`
SurfaceVariant string `json:"surfaceVariant,omitempty"`
SurfaceVariantText string `json:"surfaceVariantText,omitempty"`
SurfaceTint string `json:"surfaceTint,omitempty"`
Background string `json:"background,omitempty"`
BackgroundText string `json:"backgroundText,omitempty"`
Outline string `json:"outline,omitempty"`
SurfaceContainer string `json:"surfaceContainer,omitempty"`
SurfaceContainerHigh string `json:"surfaceContainerHigh,omitempty"`
SurfaceContainerHighest string `json:"surfaceContainerHighest,omitempty"`
Error string `json:"error,omitempty"`
Warning string `json:"warning,omitempty"`
Info string `json:"info,omitempty"`
}
type ThemeVariant struct {
ID string `json:"id"`
Name string `json:"name"`
Dark ColorScheme `json:"dark,omitempty"`
Light ColorScheme `json:"light,omitempty"`
}
type ThemeVariants struct {
Default string `json:"default,omitempty"`
Options []ThemeVariant `json:"options,omitempty"`
}
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"`
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"`
Variants *ThemeVariants `json:"variants,omitempty"`
PreviewPath string `json:"-"`
SourceDir string `json:"sourceDir,omitempty"`
}
type GitClient interface {

22
flake.lock generated
View File

@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1762835999,
"narHash": "sha256-UykYGrGFOFTmDpKTLNxj1wvd1gbDG4TkqLNSbV0TYwk=",
"lastModified": 1765838956,
"narHash": "sha256-A3a2ZfvjirX8VIdIPI+nAyukWs6vx4vet3fU0mpr7lU=",
"owner": "AvengeMedia",
"repo": "dgop",
"rev": "799301991cd5dcea9b64245f9d500dcc76615653",
"rev": "0ff697a4e3418966caa714c838fc73f1ef6ba59b",
"type": "github"
},
"original": {
@@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1764950072,
"narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
"lastModified": 1766651565,
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f61125a668a320878494449750330ca58b78c557",
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
"type": "github"
},
"original": {
@@ -43,16 +43,16 @@
]
},
"locked": {
"lastModified": 1764663772,
"narHash": "sha256-sHqLmm0wAt3PC4vczJeBozI1/f4rv9yp3IjkClHDXDs=",
"lastModified": 1766386896,
"narHash": "sha256-1uql4y229Rh+/2da99OVNe6DfsjObukXkf60TYRCvhI=",
"ref": "refs/heads/master",
"rev": "26531fc46ef17e9365b03770edd3fb9206fcb460",
"revCount": 713,
"rev": "3918290c1bcd93ed81291844d9f1ed146672dbfc",
"revCount": 714,
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell"
},
"original": {
"rev": "26531fc46ef17e9365b03770edd3fb9206fcb460",
"rev": "3918290c1bcd93ed81291844d9f1ed146672dbfc",
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell"
}

View File

@@ -8,7 +8,7 @@
inputs.nixpkgs.follows = "nixpkgs";
};
quickshell = {
url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=26531fc46ef17e9365b03770edd3fb9206fcb460";
url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=3918290c1bcd93ed81291844d9f1ed146672dbfc";
inputs.nixpkgs.follows = "nixpkgs";
};
};

View File

@@ -65,6 +65,7 @@ Singleton {
property string currentThemeName: "blue"
property string currentThemeCategory: "generic"
property string customThemeFile: ""
property var registryThemeVariants: ({})
property string matugenScheme: "scheme-tonal-spot"
property bool runUserMatugenTemplates: true
property string matugenTargetMonitor: ""
@@ -176,6 +177,7 @@ Singleton {
property bool waveProgressEnabled: true
property bool scrollTitleEnabled: true
property bool audioVisualizerEnabled: true
property bool audioScrollEnabled: true
property bool clockCompactMode: false
property bool focusedWindowCompactMode: false
property bool runningAppsCompactMode: true
@@ -915,7 +917,8 @@ Singleton {
const defaultBar = barConfigs[0] || getBarConfig("default");
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const bottomGap = Math.max(0, rawBottomGap);
const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4);
@@ -1556,6 +1559,19 @@ Singleton {
return workspaceNameIcons[workspaceName] || null;
}
function getRegistryThemeVariant(themeId, defaultVariant) {
return registryThemeVariants[themeId] || defaultVariant || "";
}
function setRegistryThemeVariant(themeId, variantId) {
var variants = JSON.parse(JSON.stringify(registryThemeVariants));
variants[themeId] = variantId;
registryThemeVariants = variants;
saveSettings();
if (typeof Theme !== "undefined")
Theme.reloadCustomThemeVariant();
}
function toggleDankBarVisible() {
const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) {

View File

@@ -93,6 +93,8 @@ Singleton {
property var _pendingGenerateParams: null
property var customThemeData: null
property var customThemeRawData: null
readonly property var currentThemeVariants: customThemeRawData?.variants || null
readonly property string currentThemeId: customThemeRawData?.id || ""
Component.onCompleted: {
Quickshell.execDetached(["mkdir", "-p", stateDir]);
@@ -604,21 +606,60 @@ Singleton {
function loadCustomTheme(themeData) {
customThemeRawData = themeData;
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
var baseColors = {};
if (themeData.dark || themeData.light) {
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light;
customThemeData = selectedTheme;
baseColors = themeData[colorMode] || themeData.dark || themeData.light || {};
} else {
customThemeData = themeData;
baseColors = themeData;
}
if (themeData.variants && themeData.variants.options && themeData.variants.options.length > 0) {
const themeId = themeData.id || "";
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
const variant = findVariant(themeData.variants.options, selectedVariantId);
if (variant) {
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
customThemeData = mergeColors(baseColors, variantColors);
generateSystemThemesFromCurrentTheme();
return;
}
}
customThemeData = baseColors;
generateSystemThemesFromCurrentTheme();
}
function findVariant(options, variantId) {
if (!variantId || !options)
return null;
for (var i = 0; i < options.length; i++) {
if (options[i].id === variantId)
return options[i];
}
return options[0] || null;
}
function mergeColors(base, overlay) {
var result = JSON.parse(JSON.stringify(base));
for (var key in overlay) {
if (overlay[key])
result[key] = overlay[key];
}
return result;
}
function loadCustomThemeFromFile(filePath) {
customThemeFileView.path = filePath;
}
function reloadCustomThemeVariant() {
if (currentTheme !== "custom" || !customThemeRawData)
return;
loadCustomTheme(customThemeRawData);
}
property alias availableThemeNames: root._availableThemeNames
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
property string currentThemeName: currentTheme
@@ -912,6 +953,16 @@ Singleton {
if (customThemeRawData && (customThemeRawData.dark || customThemeRawData.light)) {
darkTheme = customThemeRawData.dark || customThemeRawData.light;
lightTheme = customThemeRawData.light || customThemeRawData.dark;
if (customThemeRawData.variants && customThemeRawData.variants.options) {
const themeId = customThemeRawData.id || "";
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
if (variant) {
darkTheme = mergeColors(darkTheme, variant.dark || {});
lightTheme = mergeColors(lightTheme, variant.light || {});
}
}
} else {
darkTheme = customThemeData;
lightTheme = customThemeData;

View File

@@ -9,6 +9,7 @@ var SPEC = {
currentThemeName: { def: "blue", onChange: "applyStoredTheme" },
currentThemeCategory: { def: "generic" },
customThemeFile: { def: "" },
registryThemeVariants: { def: {} },
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
@@ -91,6 +92,7 @@ var SPEC = {
waveProgressEnabled: { def: true },
scrollTitleEnabled: { def: true },
audioVisualizerEnabled: { def: true },
audioScrollEnabled: { def: true },
clockCompactMode: { def: false },
focusedWindowCompactMode: { def: false },
runningAppsCompactMode: { def: true },

View File

@@ -96,12 +96,7 @@ PanelWindow {
}
}
WlrLayershell.layer: {
if ((barConfig?.autoHide ?? false) && topBarCore.reveal) {
return WlrLayer.Overlay;
}
return dBarLayer;
}
WlrLayershell.layer: dBarLayer
WlrLayershell.namespace: "dms:bar"
signal colorPickerRequested

View File

@@ -52,9 +52,12 @@ BasePill {
property real touchpadThreshold: 100
onWheel: function (wheelEvent) {
wheelEvent.accepted = true;
if (!usePlayerVolume)
return;
if (!SettingsData.audioScrollEnabled)
return;
wheelEvent.accepted = true;
const deltaY = wheelEvent.angleDelta.y;
const isMouseWheelY = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;

View File

@@ -43,6 +43,13 @@ Item {
checked: SettingsData.audioVisualizerEnabled
onToggled: checked => SettingsData.set("audioVisualizerEnabled", checked)
}
SettingsToggleRow {
text: I18n.tr("Scroll Wheel")
description: I18n.tr("Scroll on widget changes media volume")
checked: SettingsData.audioScrollEnabled
onToggled: checked => SettingsData.set("audioScrollEnabled", checked)
}
}
}
}

View File

@@ -382,13 +382,23 @@ FloatingWindow {
}
delegate: Rectangle {
id: themeDelegate
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 hasVariants: modelData.hasVariants || false
property var variants: modelData.variants || null
property string selectedVariantId: hasVariants && variants ? (variants.default || (variants.options[0]?.id ?? "")) : ""
property string previewPath: {
const baseDir = "/tmp/dankdots-plugin-registry/themes/" + (modelData.sourceDir || modelData.id);
const mode = Theme.isLightMode ? "light" : "dark";
if (hasVariants && selectedVariantId)
return baseDir + "/preview-" + selectedVariantId + "-" + mode + ".svg";
return baseDir + "/preview-" + mode + ".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)
@@ -479,6 +489,26 @@ FloatingWindow {
font.weight: Font.Medium
}
}
Rectangle {
height: 18
width: variantsText.implicitWidth + Theme.spacingS
radius: 9
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.15)
border.color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.4)
border.width: 1
visible: themeDelegate.hasVariants
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: variantsText
anchors.centerIn: parent
text: I18n.tr("%1 variants").arg(themeDelegate.variants?.options?.length ?? 0)
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondary
font.weight: Font.Medium
}
}
}
StyledText {
@@ -495,10 +525,44 @@ FloatingWindow {
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 3
maximumLineCount: themeDelegate.hasVariants ? 2 : 3
elide: Text.ElideRight
visible: modelData.description && modelData.description.length > 0
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: themeDelegate.hasVariants
Repeater {
model: themeDelegate.variants?.options ?? []
Rectangle {
property bool isActive: themeDelegate.selectedVariantId === modelData.id
height: 22
width: variantChipText.implicitWidth + Theme.spacingS * 2
radius: 11
color: isActive ? Theme.primary : Theme.surfaceContainerHigh
border.color: isActive ? Theme.primary : Theme.outline
border.width: 1
StyledText {
id: variantChipText
anchors.centerIn: parent
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
color: isActive ? Theme.primaryText : Theme.surfaceText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: themeDelegate.selectedVariantId = modelData.id
}
}
}
}
}
Rectangle {

View File

@@ -108,317 +108,184 @@ Item {
}
Column {
id: themeCategoryColumn
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
DankButtonGroup {
id: themeCategoryGroup
property bool isRegistryTheme: Theme.currentThemeCategory === "registry"
property int currentThemeIndex: {
if (isRegistryTheme)
return 4;
if (Theme.currentTheme === Theme.dynamic)
return 2;
if (Theme.currentThemeName === "custom")
return 3;
if (Theme.currentThemeCategory === "catppuccin")
return 1;
return 0;
}
property int pendingThemeIndex: -1
Item {
width: parent.width
height: themeCategoryGroup.implicitHeight
clip: true
model: DMSService.dmsAvailable ? ["Generic", "Catppuccin", "Auto", "Custom", "Registry"] : ["Generic", "Catppuccin", "Auto", "Custom"]
currentIndex: currentThemeIndex
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected)
return;
pendingThemeIndex = index;
}
onAnimationCompleted: {
if (pendingThemeIndex === -1)
return;
switch (pendingThemeIndex) {
case 0:
Theme.switchThemeCategory("generic", "blue");
break;
case 1:
Theme.switchThemeCategory("catppuccin", "cat-mauve");
break;
case 2:
if (ToastService.wallpaperErrorStatus === "matugen_missing")
ToastService.showError(I18n.tr("matugen not found - install matugen package for dynamic theming", "matugen error"));
else if (ToastService.wallpaperErrorStatus === "error")
ToastService.showError(I18n.tr("Wallpaper processing failed - check wallpaper path", "wallpaper error"));
else
Theme.switchThemeCategory("dynamic", Theme.dynamic);
break;
case 3:
Theme.switchThemeCategory("custom", "custom");
break;
case 4:
Theme.switchThemeCategory("registry", "");
break;
DankButtonGroup {
id: themeCategoryGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 420 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 420 ? 44 : 64
textSize: parent.width < 420 ? Theme.fontSizeSmall : Theme.fontSizeMedium
property bool isRegistryTheme: Theme.currentThemeCategory === "registry"
property int currentThemeIndex: {
if (isRegistryTheme)
return 4;
if (Theme.currentTheme === Theme.dynamic)
return 2;
if (Theme.currentThemeName === "custom")
return 3;
if (Theme.currentThemeCategory === "catppuccin")
return 1;
return 0;
}
property int pendingThemeIndex: -1
model: DMSService.dmsAvailable ? ["Generic", "Catppuccin", "Auto", "Custom", "Registry"] : ["Generic", "Catppuccin", "Auto", "Custom"]
currentIndex: currentThemeIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
pendingThemeIndex = index;
}
onAnimationCompleted: {
if (pendingThemeIndex === -1)
return;
switch (pendingThemeIndex) {
case 0:
Theme.switchThemeCategory("generic", "blue");
break;
case 1:
Theme.switchThemeCategory("catppuccin", "cat-mauve");
break;
case 2:
if (ToastService.wallpaperErrorStatus === "matugen_missing")
ToastService.showError(I18n.tr("matugen not found - install matugen package for dynamic theming", "matugen error"));
else if (ToastService.wallpaperErrorStatus === "error")
ToastService.showError(I18n.tr("Wallpaper processing failed - check wallpaper path", "wallpaper error"));
else
Theme.switchThemeCategory("dynamic", Theme.dynamic);
break;
case 3:
Theme.switchThemeCategory("custom", "custom");
break;
case 4:
Theme.switchThemeCategory("registry", "");
break;
}
pendingThemeIndex = -1;
}
pendingThemeIndex = -1;
}
}
Column {
Flow {
id: genericColorFlow
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
visible: Theme.currentThemeCategory === "generic" && Theme.currentTheme !== Theme.dynamic && Theme.currentThemeName !== "custom"
property int dotSize: width < 300 ? 28 : 32
Row {
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"]
Repeater {
model: ["blue", "purple", "green", "orange", "red"]
Rectangle {
required property string modelData
property string themeName: modelData
width: genericColorFlow.dotSize
height: genericColorFlow.dotSize
radius: width / 2
color: Theme.getThemeColors(themeName).primary
border.color: Theme.outline
border.width: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 2 : 1
scale: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
Rectangle {
required property string modelData
property string themeName: modelData
width: 32
height: 32
radius: 16
color: Theme.getThemeColors(themeName).primary
border.color: Theme.outline
border.width: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 2 : 1
scale: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
width: nameText.contentWidth + Theme.spacingS * 2
height: nameText.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea.containsMouse
Rectangle {
width: nameText.contentWidth + Theme.spacingS * 2
height: nameText.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea.containsMouse
StyledText {
id: nameText
text: Theme.getThemeColors(parent.parent.themeName).name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.switchTheme(parent.themeName)
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
StyledText {
id: nameText
text: Theme.getThemeColors(parent.parent.themeName).name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
}
}
Row {
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.switchTheme(parent.themeName)
}
Repeater {
model: ["cyan", "pink", "amber", "coral", "monochrome"]
Rectangle {
required property string modelData
property string themeName: modelData
width: 32
height: 32
radius: 16
color: Theme.getThemeColors(themeName).primary
border.color: Theme.outline
border.width: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 2 : 1
scale: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
Rectangle {
width: nameText2.contentWidth + Theme.spacingS * 2
height: nameText2.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea2.containsMouse
StyledText {
id: nameText2
text: Theme.getThemeColors(parent.parent.themeName).name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea2
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.switchTheme(parent.themeName)
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
Column {
Flow {
id: catColorFlow
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
visible: Theme.currentThemeCategory === "catppuccin" && Theme.currentTheme !== Theme.dynamic && Theme.currentThemeName !== "custom"
property int dotSize: width < 300 ? 28 : 32
Row {
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: ["cat-rosewater", "cat-flamingo", "cat-pink", "cat-mauve", "cat-red", "cat-maroon", "cat-peach", "cat-yellow", "cat-green", "cat-teal", "cat-sky", "cat-sapphire", "cat-blue", "cat-lavender"]
Repeater {
model: ["cat-rosewater", "cat-flamingo", "cat-pink", "cat-mauve", "cat-red", "cat-maroon", "cat-peach"]
Rectangle {
required property string modelData
property string themeName: modelData
width: catColorFlow.dotSize
height: catColorFlow.dotSize
radius: width / 2
color: Theme.getCatppuccinColor(themeName)
border.color: Theme.outline
border.width: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 2 : 1
scale: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
Rectangle {
required property string modelData
property string themeName: modelData
width: 32
height: 32
radius: 16
color: Theme.getCatppuccinColor(themeName)
border.color: Theme.outline
border.width: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 2 : 1
scale: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
width: nameTextCat.contentWidth + Theme.spacingS * 2
height: nameTextCat.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseAreaCat.containsMouse
Rectangle {
width: nameTextCat.contentWidth + Theme.spacingS * 2
height: nameTextCat.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseAreaCat.containsMouse
StyledText {
id: nameTextCat
text: Theme.getCatppuccinVariantName(parent.parent.themeName)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseAreaCat
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.switchTheme(parent.themeName)
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
StyledText {
id: nameTextCat
text: Theme.getCatppuccinVariantName(parent.parent.themeName)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
}
}
Row {
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
MouseArea {
id: mouseAreaCat
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.switchTheme(parent.themeName)
}
Repeater {
model: ["cat-yellow", "cat-green", "cat-teal", "cat-sky", "cat-sapphire", "cat-blue", "cat-lavender"]
Rectangle {
required property string modelData
property string themeName: modelData
width: 32
height: 32
radius: 16
color: Theme.getCatppuccinColor(themeName)
border.color: Theme.outline
border.width: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 2 : 1
scale: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
Rectangle {
width: nameTextCat2.contentWidth + Theme.spacingS * 2
height: nameTextCat2.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseAreaCat2.containsMouse
StyledText {
id: nameTextCat2
text: Theme.getCatppuccinVariantName(parent.parent.themeName)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseAreaCat2
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.switchTheme(parent.themeName)
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
@@ -608,7 +475,10 @@ Item {
visible: Theme.currentThemeCategory === "registry"
Grid {
columns: 3
id: themeGrid
property int cardWidth: registrySection.width < 350 ? 100 : 140
property int cardHeight: registrySection.width < 350 ? 72 : 100
columns: Math.max(1, Math.floor((registrySection.width + spacing) / (cardWidth + spacing)))
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: themeColorsTab.installedRegistryThemes.length > 0
@@ -619,9 +489,18 @@ Item {
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
property bool hasVariants: modelData.hasVariants || false
property var variants: modelData.variants || null
property string selectedVariant: hasVariants ? SettingsData.getRegistryThemeVariant(modelData.id, variants?.default || "") : ""
property string previewPath: {
const baseDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + (modelData.sourceDir || modelData.id);
const mode = Theme.isLightMode ? "light" : "dark";
if (hasVariants && selectedVariant)
return baseDir + "/preview-" + selectedVariant + "-" + mode + ".svg";
return baseDir + "/preview-" + mode + ".svg";
}
width: themeGrid.cardWidth
height: themeGrid.cardHeight
radius: Theme.cornerRadius
color: Theme.surfaceVariant
border.color: isActive ? Theme.primary : Theme.outline
@@ -648,7 +527,7 @@ Item {
DankIcon {
anchors.centerIn: parent
name: "palette"
size: 32
size: themeGrid.cardWidth < 120 ? 24 : 32
color: Theme.primary
visible: previewImage.status === Image.Error || previewImage.status === Image.Null
}
@@ -657,18 +536,18 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 24
height: themeGrid.cardWidth < 120 ? 18 : 22
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.6)
StyledText {
anchors.centerIn: parent
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: themeGrid.cardWidth < 120 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall
color: "white"
font.weight: Font.Medium
elide: Text.ElideRight
width: parent.width - Theme.spacingS * 2
width: parent.width - Theme.spacingXS * 2
horizontalAlignment: Text.AlignHCenter
}
}
@@ -676,21 +555,40 @@ Item {
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 4
width: 20
height: 20
radius: 10
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
width: themeGrid.cardWidth < 120 ? 16 : 20
height: width
radius: width / 2
color: Theme.primary
visible: themeCard.isActive
DankIcon {
anchors.centerIn: parent
name: "check"
size: 14
size: themeGrid.cardWidth < 120 ? 10 : 14
color: Theme.surface
}
}
Rectangle {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
width: themeGrid.cardWidth < 120 ? 16 : 20
height: width
radius: width / 2
color: Theme.secondary
visible: themeCard.hasVariants && !deleteButton.visible
StyledText {
anchors.centerIn: parent
text: themeCard.variants?.options?.length || 0
font.pixelSize: themeGrid.cardWidth < 120 ? Theme.fontSizeSmall - 4 : Theme.fontSizeSmall - 2
color: Theme.surface
font.weight: Font.Bold
}
}
MouseArea {
id: cardMouseArea
anchors.fill: parent
@@ -708,10 +606,10 @@ Item {
id: deleteButton
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 4
width: 24
height: 24
radius: 12
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
width: themeGrid.cardWidth < 120 ? 18 : 24
height: width
radius: width / 2
color: deleteMouseArea.containsMouse ? Theme.error : Qt.rgba(0, 0, 0, 0.6)
opacity: cardMouseArea.containsMouse || deleteMouseArea.containsMouse ? 1 : 0
visible: opacity > 0
@@ -725,7 +623,7 @@ Item {
DankIcon {
anchors.centerIn: parent
name: "close"
size: 14
size: themeGrid.cardWidth < 120 ? 10 : 14
color: "white"
}
@@ -751,6 +649,75 @@ Item {
}
}
Column {
id: variantSelector
width: parent.width
spacing: Theme.spacingS
visible: activeThemeVariants !== null && activeThemeVariants.options && activeThemeVariants.options.length > 0
property string activeThemeId: {
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.id;
}
return "";
}
property var activeThemeVariants: {
if (!activeThemeId)
return null;
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (t.id === activeThemeId && t.hasVariants)
return t.variants;
}
return null;
}
property string selectedVariant: activeThemeId ? SettingsData.getRegistryThemeVariant(activeThemeId, activeThemeVariants?.default || "") : ""
property var variantNames: {
if (!activeThemeVariants?.options)
return [];
return activeThemeVariants.options.map(v => v.name);
}
property int selectedIndex: {
if (!activeThemeVariants?.options || !selectedVariant)
return 0;
for (var i = 0; i < activeThemeVariants.options.length; i++) {
if (activeThemeVariants.options[i].id === selectedVariant)
return i;
}
return 0;
}
Item {
width: parent.width
height: variantButtonGroup.implicitHeight
clip: true
DankButtonGroup {
id: variantButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 400 ? 44 : 64
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: variantSelector.variantNames
currentIndex: variantSelector.selectedIndex
selectionMode: "single"
onAnimationCompleted: {
if (currentIndex < 0 || !variantSelector.activeThemeVariants?.options)
return;
const variantId = variantSelector.activeThemeVariants.options[currentIndex]?.id;
if (!variantId || variantId === variantSelector.selectedVariant)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeVariant(variantSelector.activeThemeId, variantId);
}
}
}
}
StyledText {
text: I18n.tr("No themes installed. Browse themes to install from the registry.", "no registry themes installed hint")
font.pixelSize: Theme.fontSizeSmall