settings/wallpaper: more restyle to appearance header

This commit is contained in:
bbedward
2026-09-20 21:09:12 -04:00
parent d9d9e6b9cf
commit 4d6de9cf96
12 changed files with 609 additions and 299 deletions
@@ -79,6 +79,104 @@ func addVariantsInfo(info *ThemeInfo, variants *themes.ThemeVariants) {
}
}
func paletteInfo(theme *themes.Theme) *PaletteInfo {
dark, light := defaultSchemes(theme)
info := &PaletteInfo{Dark: swatchColors(dark), Light: swatchColors(light)}
if info.Dark == nil && info.Light == nil {
return nil
}
return info
}
func defaultSchemes(theme *themes.Theme) (themes.ColorScheme, themes.ColorScheme) {
v := theme.Variants
if v == nil {
return theme.Dark, theme.Light
}
if v.Type == "multi" {
return multiScheme(v, theme.Dark, false), multiScheme(v, theme.Light, true)
}
for _, opt := range v.Options {
if opt.ID == v.Default {
return overlay(theme.Dark, opt.Dark), overlay(theme.Light, opt.Light)
}
}
if len(v.Options) > 0 {
return overlay(theme.Dark, v.Options[0].Dark), overlay(theme.Light, v.Options[0].Light)
}
return theme.Dark, theme.Light
}
// variants and flavors often carry surfaces only; the accent colors stay on the base scheme
func overlay(base, top themes.ColorScheme) themes.ColorScheme {
pick := func(fallback, value string) string {
if value != "" {
return value
}
return fallback
}
base.Primary = pick(base.Primary, top.Primary)
base.Secondary = pick(base.Secondary, top.Secondary)
base.PrimaryContainer = pick(base.PrimaryContainer, top.PrimaryContainer)
base.Info = pick(base.Info, top.Info)
base.Error = pick(base.Error, top.Error)
base.Warning = pick(base.Warning, top.Warning)
return base
}
func multiScheme(v *themes.ThemeVariants, base themes.ColorScheme, light bool) themes.ColorScheme {
defaults := map[string]string{}
if v.Defaults != nil {
if light {
defaults = v.Defaults.Light
} else {
defaults = v.Defaults.Dark
}
}
scheme := base
flavorID := defaults["flavor"]
for _, f := range v.Flavors {
if f.ID != flavorID {
continue
}
if light {
scheme = overlay(base, f.Light)
} else {
scheme = overlay(base, f.Dark)
}
break
}
for _, a := range v.Accents {
if a.ID != defaults["accent"] {
continue
}
if accent, ok := a.FlavorColors[flavorID]; ok {
scheme = overlay(scheme, accent)
}
break
}
return scheme
}
func swatchColors(scheme themes.ColorScheme) map[string]string {
if scheme.Primary == "" {
return nil
}
colors := map[string]string{"primary": scheme.Primary}
for key, value := range map[string]string{
"secondary": scheme.Secondary,
"primaryContainer": scheme.PrimaryContainer,
"info": scheme.Info,
"error": scheme.Error,
"warning": scheme.Warning,
} {
if value != "" {
colors[key] = value
}
}
return colors
}
func HandleListInstalled(conn *ipc.ConnWriter, req ipc.Request) {
manager, err := themes.NewManager()
if err != nil {
@@ -120,6 +218,7 @@ func HandleListInstalled(conn *ipc.ConnWriter, req ipc.Request) {
HasUpdate: hasUpdate,
}
addVariantsInfo(&info, theme.Variants)
info.Palette = paletteInfo(&theme)
result = append(result, info)
} else {
installed, err := manager.GetInstalledTheme(id)
@@ -141,6 +240,7 @@ func HandleListInstalled(conn *ipc.ConnWriter, req ipc.Request) {
FirstParty: isFirstParty(installed.Author),
}
addVariantsInfo(&info, installed.Variants)
info.Palette = paletteInfo(installed)
result = append(result, info)
}
}
@@ -0,0 +1,56 @@
package themes
import (
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
"github.com/stretchr/testify/require"
)
func TestPaletteInfoResolvesDefaultFlavorAndAccent(t *testing.T) {
theme := &themes.Theme{
Variants: &themes.ThemeVariants{
Type: "multi",
Defaults: &themes.MultiVariantDefaults{
Dark: map[string]string{"flavor": "mocha", "accent": "mauve"},
Light: map[string]string{"flavor": "latte", "accent": "mauve"},
},
Flavors: []themes.ThemeFlavor{
{ID: "latte", Light: themes.ColorScheme{Surface: "#eff1f5", Info: "#04a5e5"}},
{ID: "mocha", Dark: themes.ColorScheme{Surface: "#1e1e2e", Info: "#89dceb"}},
},
Accents: []themes.ThemeAccent{
{ID: "rosewater", FlavorColors: map[string]themes.ColorScheme{"mocha": {Primary: "#f5e0dc"}}},
{ID: "mauve", FlavorColors: map[string]themes.ColorScheme{
"mocha": {Primary: "#cba6f7", Secondary: "#f5c2e7"},
"latte": {Primary: "#8839ef", Secondary: "#ea76cb"},
}},
},
},
}
palette := paletteInfo(theme)
require.NotNil(t, palette)
require.Equal(t, map[string]string{"primary": "#cba6f7", "secondary": "#f5c2e7", "info": "#89dceb"}, palette.Dark)
require.Equal(t, map[string]string{"primary": "#8839ef", "secondary": "#ea76cb", "info": "#04a5e5"}, palette.Light)
}
func TestPaletteInfoOverlaysDefaultOptionOnBase(t *testing.T) {
theme := &themes.Theme{
Dark: themes.ColorScheme{Primary: "#000000", Secondary: "#111111"},
Light: themes.ColorScheme{Primary: "#eeeeee"},
Variants: &themes.ThemeVariants{
Default: "ocean",
Options: []themes.ThemeVariant{
{ID: "forest", Dark: themes.ColorScheme{Primary: "#00ff00"}},
{ID: "ocean", Dark: themes.ColorScheme{Surface: "#001020", Secondary: "#0000ff"}, Light: themes.ColorScheme{Primary: "#0000aa"}},
},
},
}
palette := paletteInfo(theme)
require.Equal(t, map[string]string{"primary": "#000000", "secondary": "#0000ff"}, palette.Dark)
require.Equal(t, map[string]string{"primary": "#0000aa"}, palette.Light)
}
+6
View File
@@ -35,6 +35,11 @@ type VariantsInfo struct {
Accents []AccentInfo `json:"accents,omitempty"`
}
type PaletteInfo struct {
Dark map[string]string `json:"dark,omitempty"`
Light map[string]string `json:"light,omitempty"`
}
type ThemeInfo struct {
ID string `json:"id"`
Name string `json:"name"`
@@ -48,5 +53,6 @@ type ThemeInfo struct {
HasUpdate bool `json:"hasUpdate,omitempty"`
HasVariants bool `json:"hasVariants,omitempty"`
Variants *VariantsInfo `json:"variants,omitempty"`
Palette *PaletteInfo `json:"palette,omitempty"`
WCAG *themes.ThemeWCAG `json:"wcag,omitempty"`
}
+4 -1
View File
@@ -24,13 +24,14 @@ Singleton {
readonly property var _pinKeys: ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"]
readonly property var _historyKeys: ["browserUsageHistory", "filePickerUsageHistory"]
readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings", "processFilterTypes", "pluginViewSort", "pluginViewFilter", "dashFocusCardId", "mediaLyricsOpen"].concat(_pinKeys, _historyKeys)
readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings", "processFilterTypes", "pluginViewSort", "pluginViewFilter", "dashFocusCardId", "mediaLyricsOpen", "matugenPreviews"].concat(_pinKeys, _historyKeys)
property string wallpaperLastPath: ""
property string profileLastPath: ""
property string pluginViewFilter: "enabled"
property string dashFocusCardId: ""
property bool mediaLyricsOpen: false
property var matugenPreviews: ({})
property var pluginViewSort: ({
by: "modified",
descending: true
@@ -182,6 +183,7 @@ Singleton {
pluginViewFilter = ["all", "enabled", "disabled", "updates"].includes(cache.pluginViewFilter) ? cache.pluginViewFilter : "enabled";
dashFocusCardId = typeof cache.dashFocusCardId === "string" ? cache.dashFocusCardId : "";
mediaLyricsOpen = cache.mediaLyricsOpen === true;
matugenPreviews = typeof cache.matugenPreviews?.key === "string" ? cache.matugenPreviews : {};
const pluginSort = cache.pluginViewSort;
pluginViewSort = {
by: ["name", "author", "modified"].includes(pluginSort?.by) ? pluginSort.by : "modified",
@@ -248,6 +250,7 @@ Singleton {
"pluginViewFilter": pluginViewFilter,
"dashFocusCardId": dashFocusCardId,
"mediaLyricsOpen": mediaLyricsOpen,
"matugenPreviews": matugenPreviews,
"fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion
};
+10 -146
View File
@@ -15,82 +15,17 @@ Item {
property var parentModal: null
property string pendingExtractJson: ""
property var cachedSourceModes: Theme.availableSourceModes.map(option => option.label)
property var matugenSchemePreviews: ({})
property string matugenPreviewRequestKey: ""
property string matugenPreviewLoadedKey: ""
readonly property string matugenPreviewSource: SettingsData.matugenSeedColor || Theme.getMatugenColor("source_color", Theme.primary).toString()
readonly property string matugenPreviewImage: (!SettingsData.matugenSeedColor && Theme.rawWallpaperPath && !Theme.rawWallpaperPath.startsWith("#")) ? Theme.rawWallpaperPath : ""
readonly property string matugenPreviewKey: matugenPreviewSource + "|" + (SettingsData.matugenContrast ?? 0) + "|" + matugenPreviewImage + "|" + SettingsData.matugenSpec
readonly property var currentPalette: ThemePalette.pick({
"primary": Theme.primary,
"secondary": Theme.secondary,
"tertiary": Theme.tertiary,
"primaryContainer": Theme.primaryContainer,
"info": Theme.info,
"error": Theme.error,
"warning": Theme.warning
})
property bool matugenPreviewFailed: false
readonly property bool matugenPreviewsReady: matugenPreviewLoadedKey === matugenPreviewKey || matugenPreviewFailed || !Theme.matugenAvailable
onMatugenPreviewKeyChanged: refreshMatugenSchemePreviews()
readonly property string matugenPreviewKey: MatugenPreviewService.key
readonly property bool matugenAvailable: Theme.matugenAvailable
onMatugenPreviewKeyChanged: MatugenPreviewService.refresh()
onMatugenAvailableChanged: MatugenPreviewService.refresh()
property var installedRegistryThemes: []
readonly property var matugenSchemeOptions: {
const mode = SessionData.isLightMode ? "light" : "dark";
const options = [];
for (const option of Theme.availableMatugenSchemes) {
if (option.value === "scheme-smart" && !DMSService.matugenSmartSupported)
continue;
const colors = (matugenSchemePreviews[option.value] ?? matugenSchemePreviews["scheme-tonal-spot"])?.[mode];
// a dms binary older than the tri-color preview returns the primary hex as a plain string
const primary = typeof colors === "string" ? colors : (colors?.primary ?? Theme.primary.toString());
options.push({
"value": option.value,
"label": option.label,
"primary": primary,
"secondary": colors?.secondary ?? primary,
"tertiary": colors?.tertiary ?? primary
});
}
return options;
}
function refreshMatugenSchemePreviews() {
if (!Theme.matugenAvailable)
return;
const requestKey = matugenPreviewKey;
if (requestKey === matugenPreviewLoadedKey || requestKey === matugenPreviewRequestKey)
return;
matugenPreviewRequestKey = requestKey;
matugenPreviewFailed = false;
const args = [Proc.dmsBin, "matugen", "preview", "--source-color", matugenPreviewSource, "--contrast", String(SettingsData.matugenContrast ?? 0)];
if (matugenPreviewImage)
args.push("--image", matugenPreviewImage);
if (SettingsData.matugenSpec === "2025")
args.push("--spec", "2025");
Proc.runCommand("", args, (output, exitCode) => {
if (requestKey !== themeColorsTab.matugenPreviewRequestKey)
return;
themeColorsTab.matugenPreviewRequestKey = "";
if (exitCode !== 0) {
themeColorsTab.matugenPreviewFailed = true;
return;
}
try {
themeColorsTab.matugenSchemePreviews = JSON.parse(output.trim());
themeColorsTab.matugenPreviewLoadedKey = requestKey;
} catch (e) {
themeColorsTab.matugenPreviewFailed = true;
}
});
}
Component.onCompleted: {
if (DMSService.dmsAvailable)
DMSService.listInstalledThemes();
if (PopoutService.pendingThemeInstall)
Qt.callLater(() => showThemeBrowser());
refreshMatugenSchemePreviews();
MatugenPreviewService.refresh();
}
Connections {
@@ -108,13 +43,6 @@ Item {
}
}
Connections {
target: Theme
function onMatugenAvailableChanged() {
themeColorsTab.refreshMatugenSchemePreviews();
}
}
SettingsPage {
id: mainColumn
@@ -237,73 +165,9 @@ Item {
}
}
Item {
SettingsThemeColorDots {
width: parent.width
height: genericColorGrid.implicitHeight + Math.ceil(genericColorGrid.dotSize * 0.05)
visible: Theme.currentThemeCategory === "generic" && Theme.currentTheme !== Theme.dynamic && Theme.currentThemeName !== "custom"
Grid {
id: genericColorGrid
property var colorList: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"]
property int dotSize: Theme.minimumTouchTargetSize
columns: Math.ceil(colorList.length / 2)
rowSpacing: Theme.spacingS
columnSpacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: genericColorGrid.colorList
Rectangle {
required property string modelData
property string themeName: modelData
readonly property var colors: Theme.getThemeColors(themeName)
readonly property var palette: ThemePalette.pick(colors)
readonly property bool isActive: Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic
width: genericColorGrid.dotSize
height: genericColorGrid.dotSize
radius: width / 2
color: "transparent"
scale: isActive ? 1.1 : 1
DankPaletteSwatch {
anchors.fill: parent
primaryColor: parent.palette.primary
secondaryColor: parent.palette.secondary
tertiaryColor: parent.palette.tertiary
}
DankIcon {
anchors.centerIn: parent
name: "check"
size: Theme.iconSizeMedium
color: Theme.isLightColor(parent.colors.primary) ? Theme.contrastDark : Theme.contrastLight
visible: parent.isActive
}
DankTooltipHost {
text: parent.colors.name
target: parent
hoverArea: mouseArea
}
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
}
}
}
}
}
}
Row {
@@ -431,10 +295,10 @@ Item {
SettingsSwatchGrid {
id: schemeGrid
options: themeColorsTab.matugenSchemeOptions
options: MatugenPreviewService.schemeOptions
currentValue: SettingsData.matugenScheme
enabled: themeColorsTab.matugenPreviewsReady
opacity: themeColorsTab.matugenPreviewsReady ? 1 : Theme.pendingOpacity
enabled: MatugenPreviewService.ready
opacity: MatugenPreviewService.ready ? 1 : Theme.pendingOpacity
onSelected: value => SettingsData.setMatugenScheme(value)
Behavior on opacity {
@@ -446,7 +310,7 @@ Item {
DankSpinner {
anchors.centerIn: parent
running: !themeColorsTab.matugenPreviewsReady
running: !MatugenPreviewService.ready
visible: running
}
}
+194 -116
View File
@@ -39,7 +39,40 @@ Column {
"error": Theme.error,
"warning": Theme.warning
})
readonly property string modeLabel: SessionData.isLightMode ? I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode") : I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode")
readonly property bool dynamicTheme: Theme.currentTheme === Theme.dynamic && Theme.currentThemeCategory !== "registry"
readonly property bool genericTheme: Theme.currentThemeCategory === "generic" && Theme.currentTheme !== Theme.dynamic && Theme.currentThemeName !== "custom"
readonly property string themesDir: Quickshell.env("HOME") + "/.config/DankMaterialShell/themes"
readonly property var installedRegistryThemes: DMSService.installedThemes
readonly property var registryOptions: {
const mode = Theme.isLightMode ? "light" : "dark";
return installedRegistryThemes.map(theme => {
const palette = ThemePalette.pick(theme.palette?.[mode] ?? theme.palette?.dark) ?? {};
return {
"value": theme.sourceDir || theme.id,
"label": theme.name,
"primary": palette.primary ?? Theme.primary.toString(),
"secondary": palette.secondary,
"tertiary": palette.tertiary
};
});
}
readonly property string activeRegistryTheme: {
if (Theme.currentThemeName !== "custom" || !SettingsData.customThemeFile)
return "";
for (const theme of installedRegistryThemes) {
if (SettingsData.customThemeFile.endsWith((theme.sourceDir || theme.id) + "/theme.json"))
return theme.sourceDir || theme.id;
}
return "";
}
readonly property bool registryGridShown: !dynamicTheme && !genericTheme && registryOptions.length > 0
readonly property bool registryPending: !dynamicTheme && !genericTheme && DMSService.dmsAvailable && !DMSService.installedThemesLoaded
readonly property string matugenPreviewKey: MatugenPreviewService.key
readonly property bool matugenAvailable: Theme.matugenAvailable
onMatugenPreviewKeyChanged: refreshPreviews()
onMatugenAvailableChanged: refreshPreviews()
onDynamicThemeChanged: refreshPreviews()
onVisibleChanged: refreshPreviews()
readonly property string colorModeStatus: {
if (SettingsData.matugenSmartMode)
return I18n.tr("Wallpaper");
@@ -58,6 +91,14 @@ Column {
WallpaperCyclingService.cyclingActive;
SettingsData.detectAvailableIconThemes();
SettingsData.detectAvailableCursorThemes();
refreshPreviews();
if (DMSService.dmsAvailable)
DMSService.listInstalledThemes();
}
function refreshPreviews() {
if (visible && dynamicTheme)
MatugenPreviewService.refresh();
}
function firstScreenName() {
@@ -164,89 +205,11 @@ Column {
ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set one of:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct OR\nQT_QPA_PLATFORMTHEME=qtengine\nas environment variables, and then restart the shell.\n\nOnly qt6ct requires qt6ct-kde to be installed.", "qt theme env error body"));
}
component QuickTile: Rectangle {
id: tile
property string title: ""
property string hint: ""
default property alias leading: leadingSlot.data
signal clicked
width: parent.width
height: Theme.listItemTwoLineHeight
radius: Theme.cornerRadiusM
color: Theme.foregroundColor(Theme.chipSurface, true)
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: title
Accessible.description: hint
Accessible.onPressAction: tile.clicked()
Keys.onSpacePressed: tile.clicked()
Keys.onReturnPressed: tile.clicked()
FocusRing {}
Item {
id: leadingSlot
width: Theme.avatarSize
height: Theme.avatarSize
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.left: leadingSlot.right
anchors.leftMargin: Theme.spacingM
anchors.right: chevron.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
StyledText {
width: parent.width
text: tile.title
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: tile.hint
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
DankIcon {
id: chevron
name: "chevron_right"
size: Theme.iconSize
color: Theme.surfaceVariantText
rotation: I18n.isRtl ? 180 : 0
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
StateLayer {
stateColor: Theme.surfaceText
cornerRadius: tile.radius
onClicked: tile.clicked()
}
}
SettingsCard {
tab: "wallpaper"
tags: ["background", "image", "picture", "light", "dark", "mode", "theme", "color", "palette"]
title: I18n.tr("Appearance")
settingKey: "wallpaper"
SettingsDropdownRow {
tab: "wallpaper"
tags: ["monitor", "display", "screen"]
@@ -272,72 +235,187 @@ Column {
SettingsWallpaperThumb {
width: hero.thumbWidth
height: {
const natural = width * SettingsMetrics.wallpaperThumbRatio;
if (hero.stacked)
return natural;
return root.registryPending ? Math.max(natural, side.implicitHeight) : side.implicitHeight;
}
path: root.currentWallpaper
badge: root.perMode ? root.modeLabel : ""
onBrowse: root.openBrowser()
onPickColor: root.pickColor()
onClear: root.clearWallpaper()
}
Column {
id: side
width: hero.stacked ? hero.width : hero.width - hero.thumbWidth - hero.spacing
spacing: Theme.spacingM
DankButtonGroup {
Row {
id: modeRow
width: parent.width
fillWidth: true
checkEnabled: false
model: [
{
"text": I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode"),
"icon": "light_mode"
},
{
"text": I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode"),
"icon": "dark_mode"
spacing: Theme.spacingS
DankButtonGroup {
width: parent.width - scheduleButton.width - parent.spacing
fillWidth: true
checkEnabled: false
anchors.verticalCenter: parent.verticalCenter
model: [
{
"text": I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode"),
"icon": "light_mode"
},
{
"text": I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode"),
"icon": "dark_mode"
}
]
currentIndex: SessionData.isLightMode ? 0 : 1
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
const light = index === 0;
if (light === SessionData.isLightMode)
return;
Theme.screenTransition();
Theme.setLightMode(light);
}
]
currentIndex: SessionData.isLightMode ? 0 : 1
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
const light = index === 0;
if (light === SessionData.isLightMode)
return;
Theme.screenTransition();
Theme.setLightMode(light);
}
DankActionButton {
id: scheduleButton
iconName: "schedule"
anchors.verticalCenter: parent.verticalCenter
Accessible.name: I18n.tr("Dark mode")
tooltipText: root.colorModeStatus
onClicked: root.parentModal?.navigateTo("theme_schedule")
}
}
QuickTile {
title: I18n.tr("Dark mode")
hint: root.colorModeStatus
onClicked: root.parentModal?.navigateTo("theme_schedule")
SettingsSwatchGrid {
width: parent.width
visible: root.dynamicTheme
compact: true
options: MatugenPreviewService.schemeOptions
currentValue: SettingsData.matugenScheme
enabled: Theme.matugenAvailable && MatugenPreviewService.ready
opacity: MatugenPreviewService.ready ? 1 : Theme.pendingOpacity
onSelected: value => SettingsData.setMatugenScheme(value)
DankIcon {
anchors.centerIn: parent
name: "schedule"
size: Theme.iconSize
color: Theme.primary
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
QuickTile {
title: I18n.tr("Theme & colors")
hint: Theme.currentThemeLabel
onClicked: root.parentModal?.navigateTo("theme")
SettingsThemeColorDots {
width: parent.width
visible: root.genericTheme
}
SettingsSwatchGrid {
width: parent.width
visible: root.registryGridShown
compact: true
maxHeight: hero.stacked ? 0 : hero.thumbWidth * SettingsMetrics.wallpaperThumbRatio - modeRow.height - side.spacing
options: root.registryOptions
currentValue: root.activeRegistryTheme
onSelected: value => {
SettingsData.set("customThemeFile", root.themesDir + "/" + value + "/theme.json");
Theme.switchTheme("custom", true, true);
}
}
Rectangle {
id: themeTile
width: parent.width
visible: !root.dynamicTheme && !root.genericTheme && !root.registryGridShown && !root.registryPending
height: hero.stacked ? Theme.listItemTwoLineHeight : Math.max(Theme.listItemTwoLineHeight, hero.thumbWidth * SettingsMetrics.wallpaperThumbRatio - modeRow.height - side.spacing)
radius: Theme.cornerRadiusM
color: Theme.foregroundColor(Theme.chipSurface, true)
activeFocusOnTab: visible
Accessible.role: Accessible.Button
Accessible.name: I18n.tr("Theme & colors")
Accessible.description: Theme.currentThemeLabel
Accessible.onPressAction: root.parentModal?.navigateTo("theme")
Keys.onSpacePressed: root.parentModal?.navigateTo("theme")
Keys.onReturnPressed: root.parentModal?.navigateTo("theme")
FocusRing {}
DankPaletteSwatch {
anchors.fill: parent
id: themeSwatch
width: Theme.avatarSize
height: Theme.avatarSize
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
primaryColor: root.themePalette.primary
secondaryColor: root.themePalette.secondary
tertiaryColor: root.themePalette.tertiary
}
Column {
anchors.left: themeSwatch.right
anchors.leftMargin: Theme.spacingM
anchors.right: themeChevron.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
StyledText {
width: parent.width
text: I18n.tr("Theme & colors")
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: Theme.currentThemeLabel
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
DankIcon {
id: themeChevron
name: "chevron_right"
size: Theme.iconSize
color: Theme.surfaceVariantText
rotation: I18n.isRtl ? 180 : 0
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
StateLayer {
stateColor: Theme.surfaceText
cornerRadius: themeTile.radius
onClicked: root.parentModal?.navigateTo("theme")
}
}
}
}
}
SettingsNavRow {
tab: "wallpaper"
tags: ["theme", "color", "palette", "matugen", "dynamic", "custom", "registry"]
settingKey: "themeNav"
iconName: "format_paint"
title: I18n.tr("Theme & colors")
hint: I18n.tr("Choose and install themes", "theme and colors row hint")
onClicked: root.parentModal?.navigateTo("theme")
}
}
SettingsCard {
@@ -10,17 +10,22 @@ Flow {
property var options: []
property string currentValue: ""
property real minTileWidth: SettingsMetrics.swatchTileMinWidth
property bool compact: false
property real maxHeight: 0
property real minTileWidth: compact ? Theme.minimumTouchTargetSize + Theme.spacingM : SettingsMetrics.swatchTileMinWidth
signal selected(string value)
readonly property int columns: Math.max(1, Math.floor((width + spacing) / (minTileWidth + spacing)))
readonly property real tileWidth: Math.floor((width - spacing * (columns - 1)) / columns)
readonly property real compactTileHeight: Theme.minimumTouchTargetSize + Theme.spacingS * 2
readonly property int maxRows: maxHeight > 0 && compact ? Math.max(1, Math.floor((maxHeight + spacing) / (compactTileHeight + spacing))) : 0
readonly property var shownOptions: maxRows > 0 ? options.slice(0, columns * maxRows) : options
width: parent?.width ?? 0
spacing: Theme.spacingS
Repeater {
model: root.options
model: root.shownOptions
Rectangle {
id: tile
@@ -29,7 +34,7 @@ Flow {
readonly property bool isActive: root.currentValue === modelData.value
width: root.tileWidth
height: swatch.height + label.implicitHeight + Theme.spacingS * 3
height: root.compact ? root.compactTileHeight : swatch.height + label.implicitHeight + Theme.spacingS * 3
radius: Theme.cornerRadiusM
color: Theme.floatingWindowNestedSurface
border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth
@@ -64,8 +69,15 @@ Flow {
tertiaryColor: tile.modelData.tertiary ?? tile.modelData.secondary ?? tile.modelData.primary
}
DankTooltipHost {
text: root.compact ? tile.modelData.label : null
target: tile
hoverArea: tileMouse
}
StyledText {
id: label
visible: !root.compact
anchors.top: swatch.bottom
anchors.topMargin: Theme.spacingS
anchors.left: parent.left
@@ -74,7 +86,7 @@ Flow {
anchors.rightMargin: Theme.spacingXS
text: tile.modelData.label
font.pixelSize: Theme.fontSizeSmall
font.weight: tile.isActive ? Theme.fontWeightMedium : Theme.fontWeightNormal
font.weight: tile.isActive ? Theme.fontWeightMedium : Theme.fontWeight
color: tile.isActive ? Theme.primary : Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
@@ -0,0 +1,73 @@
import QtQuick
import qs.Common
import qs.Widgets
import "../../../Common/ThemePalette.js" as ThemePalette
Item {
id: root
readonly property var colorList: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"]
readonly property int dotSize: Theme.minimumTouchTargetSize
implicitHeight: grid.implicitHeight + Math.ceil(dotSize * 0.05)
Grid {
id: grid
columns: Math.ceil(root.colorList.length / 2)
rowSpacing: Theme.spacingS
columnSpacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: root.colorList
Rectangle {
required property string modelData
readonly property var colors: Theme.getThemeColors(modelData)
readonly property var palette: ThemePalette.pick(colors)
readonly property bool isActive: Theme.currentThemeName === modelData && Theme.currentTheme !== Theme.dynamic
width: root.dotSize
height: root.dotSize
radius: width / 2
color: "transparent"
scale: isActive ? 1.1 : 1
DankPaletteSwatch {
anchors.fill: parent
primaryColor: parent.palette.primary
secondaryColor: parent.palette.secondary
tertiaryColor: parent.palette.tertiary
}
DankIcon {
anchors.centerIn: parent
name: "check"
size: Theme.iconSizeMedium
color: Theme.isLightColor(parent.colors.primary) ? Theme.contrastDark : Theme.contrastLight
visible: parent.isActive
}
DankTooltipHost {
text: parent.colors.name
target: parent
hoverArea: mouseArea
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.switchTheme(parent.modelData)
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
@@ -6,7 +6,6 @@ import qs.Widgets
Rectangle {
id: root
property string badge: ""
property string path: ""
property string placeholderIcon: "image"
property string emptyText: I18n.tr("Not set", "wallpaper not set label")
@@ -111,20 +110,28 @@ Rectangle {
Rectangle {
id: chip
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingS
width: Math.min(chipText.implicitWidth + Theme.spacingS * 2, parent.width - Theme.spacingS * 2)
// sized from unelided metrics: the elided text's own implicitWidth shrinks with the chip and collapses it
width: Math.min(chipMetrics.advanceWidth + Theme.spacingS * 2, parent.width - Theme.spacingS * 2)
height: chipText.implicitHeight + Theme.spacingXS * 2
radius: Theme.cornerRadiusS
color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha)
TextMetrics {
id: chipMetrics
font: chipText.font
text: chipMetrics.text
}
StyledText {
id: chipText
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
text: root.badge !== "" ? root.badge + " · " + root.fileName : root.fileName
text: chipMetrics.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.contrastLight
elide: Text.ElideMiddle
+2
View File
@@ -27,6 +27,7 @@ Singleton {
property var registries: []
property var availableThemes: []
property var installedThemes: []
property bool installedThemesLoaded: false
property bool isConnected: false
readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
property bool subscribeConnected: false
@@ -672,6 +673,7 @@ Singleton {
sendRequest("themes.listInstalled", null, response => {
if (response.result) {
installedThemes = response.result;
installedThemesLoaded = true;
installedThemesReceived(response.result);
}
if (callback) {
@@ -0,0 +1,75 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Common
import qs.Services
Singleton {
id: root
property var previews: CacheData.matugenPreviews.previews ?? ({})
property string requestKey: ""
property string loadedKey: CacheData.matugenPreviews.key ?? ""
property bool failed: false
readonly property string source: SettingsData.matugenSeedColor || Theme.getMatugenColor("source_color", Theme.primary).toString()
readonly property string image: (!SettingsData.matugenSeedColor && Theme.rawWallpaperPath && !Theme.rawWallpaperPath.startsWith("#")) ? Theme.rawWallpaperPath : ""
readonly property string key: source + "|" + (SettingsData.matugenContrast ?? 0) + "|" + image + "|" + SettingsData.matugenSpec
readonly property bool ready: loadedKey === key || failed || !Theme.matugenAvailable
readonly property var schemeOptions: {
const mode = SessionData.isLightMode ? "light" : "dark";
const options = [];
for (const option of Theme.availableMatugenSchemes) {
if (option.value === "scheme-smart" && !DMSService.matugenSmartSupported)
continue;
const colors = (previews[option.value] ?? previews["scheme-tonal-spot"])?.[mode];
// a dms binary older than the tri-color preview returns the primary hex as a plain string
const primary = typeof colors === "string" ? colors : (colors?.primary ?? Theme.primary.toString());
options.push({
"value": option.value,
"label": option.label,
"primary": primary,
"secondary": colors?.secondary ?? primary,
"tertiary": colors?.tertiary ?? primary
});
}
return options;
}
function refresh() {
if (!Theme.matugenAvailable)
return;
const wanted = key;
if (wanted === loadedKey || wanted === requestKey)
return;
requestKey = wanted;
failed = false;
const args = [Proc.dmsBin, "matugen", "preview", "--source-color", source, "--contrast", String(SettingsData.matugenContrast ?? 0)];
if (image)
args.push("--image", image);
if (SettingsData.matugenSpec === "2025")
args.push("--spec", "2025");
Proc.runCommand("", args, (output, exitCode) => {
if (wanted !== root.requestKey)
return;
root.requestKey = "";
if (exitCode !== 0) {
root.failed = true;
return;
}
try {
root.previews = JSON.parse(output.trim());
root.loadedKey = wanted;
CacheData.matugenPreviews = {
"key": wanted,
"previews": root.previews
};
CacheData.saveCache();
} catch (e) {
root.failed = true;
}
});
}
}
@@ -130,7 +130,8 @@
"tint",
"wallpaper"
],
"page": "personalization"
"page": "personalization",
"icon": "schedule"
},
{
"section": "_hub_applications",
@@ -712,34 +713,6 @@
],
"page": "personalization"
},
{
"section": "colorMode",
"label": "Dark mode",
"tabIndex": -1,
"category": "Wallpaper & colors",
"keywords": [
"background",
"bg",
"color",
"colors",
"colour",
"colours",
"dark",
"dark mode",
"desktop",
"hue",
"image",
"light",
"mode",
"night",
"palette",
"picture",
"theme",
"tint",
"wallpaper"
],
"page": "personalization"
},
{
"section": "iconThemeDark",
"label": "Dark mode theme",
@@ -2131,6 +2104,67 @@
],
"icon": "memory"
},
{
"section": "colorMode",
"label": "Theme & colors",
"tabIndex": -1,
"category": "Wallpaper & colors",
"keywords": [
"appearance",
"background",
"bg",
"color",
"colors",
"colour",
"colours",
"dark",
"desktop",
"hue",
"image",
"light",
"look",
"mode",
"palette",
"picture",
"style",
"theme",
"tint",
"wallpaper"
],
"page": "personalization",
"icon": "schedule"
},
{
"section": "themeNav",
"label": "Theme & colors",
"tabIndex": -1,
"category": "Wallpaper & colors",
"keywords": [
"appearance",
"background",
"bg",
"color",
"colors",
"colour",
"colours",
"custom",
"desktop",
"dynamic",
"hue",
"image",
"look",
"matugen",
"palette",
"picture",
"registry",
"style",
"theme",
"tint",
"wallpaper"
],
"page": "personalization",
"icon": "format_paint"
},
{
"section": "focusRingWidth",
"label": "Thickness",