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

themes: support for variants

This commit is contained in:
bbedward
2025-12-26 14:00:14 -05:00
parent 06b14a5869
commit ef52ce0990
10 changed files with 514 additions and 348 deletions

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 {

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: ""
@@ -1556,6 +1557,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" },

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