diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 85565f8a9..48094ed46 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -201,6 +201,10 @@ Singleton { function getMatugenColor(path, fallback) { const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"; + return getMatugenColorForMode(colorMode, path, fallback); + } + + function getMatugenColorForMode(colorMode, path, fallback) { let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]; for (const part of path.split(".")) { if (!cur || typeof cur !== "object" || !(part in cur)) @@ -210,6 +214,82 @@ Singleton { return cur || fallback; } + function extractCurrentTheme(themeName) { + var name = themeName || "Extracted Theme"; + var dark = {}; + var light = {}; + + if (currentTheme === dynamic) { + dark = buildExtractedDynamicMode("dark", name + " Dark"); + light = buildExtractedDynamicMode("light", name + " Light"); + } else if (currentTheme === custom && customThemeRawData) { + var rawDark = customThemeRawData.dark || null; + var rawLight = customThemeRawData.light || null; + if (rawDark) { + dark = JSON.parse(JSON.stringify(rawDark)); + if (!dark.name) + dark.name = name + " Dark"; + } else if (rawLight) { + dark = buildExtractedDynamicMode("dark", name + " Dark"); + } else { + dark = currentThemeData ? JSON.parse(JSON.stringify(currentThemeData)) : {}; + dark.name = name + " Dark"; + } + if (rawLight) { + light = JSON.parse(JSON.stringify(rawLight)); + if (!light.name) + light.name = name + " Light"; + } else if (rawDark) { + light = buildExtractedDynamicMode("light", name + " Light"); + } else { + light = currentThemeData ? JSON.parse(JSON.stringify(currentThemeData)) : {}; + light.name = name + " Light"; + } + } else { + var darkTheme = StockThemes.getThemeByName(currentTheme, false); + var lightTheme = StockThemes.getThemeByName(currentTheme, true); + dark = darkTheme ? JSON.parse(JSON.stringify(darkTheme)) : {}; + light = lightTheme ? JSON.parse(JSON.stringify(lightTheme)) : {}; + dark.name = name + " Dark"; + light.name = name + " Light"; + } + + return JSON.stringify({ + dark: dark, + light: light + }, null, 2); + } + + function buildExtractedDynamicMode(colorMode, name) { + return { + "name": name, + "primary": getMatugenColorForMode(colorMode, "primary", "#42a5f5"), + "primaryText": getMatugenColorForMode(colorMode, "on_primary", "#ffffff"), + "primaryContainer": getMatugenColorForMode(colorMode, "primary_container", "#1976d2"), + "secondary": getMatugenColorForMode(colorMode, "secondary", "#8ab4f8"), + "secondaryContainer": getMatugenColorForMode(colorMode, "secondary_container", getMatugenColorForMode(colorMode, "surface_container_high", "#292b2f")), + "tertiary": getMatugenColorForMode(colorMode, "tertiary", "#efb8c8"), + "tertiaryContainer": getMatugenColorForMode(colorMode, "tertiary_container", getMatugenColorForMode(colorMode, "surface_container_high", "#292b2f")), + "surface": getMatugenColorForMode(colorMode, "surface", "#1a1c1e"), + "surfaceText": getMatugenColorForMode(colorMode, "on_background", "#e3e8ef"), + "surfaceVariant": getMatugenColorForMode(colorMode, "surface_variant", "#44464f"), + "surfaceVariantText": getMatugenColorForMode(colorMode, "on_surface_variant", "#c4c7c5"), + "surfaceTint": getMatugenColorForMode(colorMode, "surface_tint", "#8ab4f8"), + "background": getMatugenColorForMode(colorMode, "background", "#1a1c1e"), + "backgroundText": getMatugenColorForMode(colorMode, "on_background", "#e3e8ef"), + "outline": getMatugenColorForMode(colorMode, "outline", "#8e918f"), + "surfaceContainerLowest": getMatugenColorForMode(colorMode, "surface_container_lowest", "#0e1013"), + "surfaceContainerLow": getMatugenColorForMode(colorMode, "surface_container_low", "#181a1d"), + "surfaceContainer": getMatugenColorForMode(colorMode, "surface_container", "#1e2023"), + "surfaceContainerHigh": getMatugenColorForMode(colorMode, "surface_container_high", "#292b2f"), + "surfaceContainerHighest": getMatugenColorForMode(colorMode, "surface_container_highest", "#343740"), + "error": getMatugenColorForMode(colorMode, "error", "#F2B8B5"), + "warning": "#FF9800", + "info": "#2196F3", + "success": "#4CAF50" + }; + } + readonly property var currentThemeData: { if (currentTheme === "custom") { return customThemeData || StockThemes.getThemeByName("purple", isLightMode); diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index 360827215..5df1f99a1 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -1,6 +1,7 @@ import QtCore import QtQuick import Quickshell +import Quickshell.Io import Quickshell.Widgets import qs.Common import qs.Modals.FileBrowser @@ -14,6 +15,7 @@ Item { id: themeColorsTab property var parentModal: null + property string pendingExtractJson: "" readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive readonly property bool frameModeActive: SettingsData.frameEnabled property var cachedIconThemes: SettingsData.availableIconThemes @@ -687,7 +689,7 @@ Item { } Column { - width: parent.width - 120 - Theme.spacingM + width: parent.width - 120 - Theme.spacingM - 36 - Theme.spacingM spacing: Theme.spacingS anchors.verticalCenter: parent.verticalCenter @@ -709,6 +711,7 @@ Item { } StyledText { + id: wallpaperPathText text: { if (ToastService.wallpaperErrorStatus === "error") return I18n.tr("Wallpaper processing failed", "wallpaper processing error"); @@ -726,6 +729,22 @@ Item { wrapMode: Text.WordWrap } } + + DankActionButton { + buttonSize: 36 + iconName: "download" + iconSize: Theme.iconSize + backgroundColor: Theme.primaryHover + iconColor: Theme.primary + tooltipText: I18n.tr("Extract theme to JSON", "extract theme tooltip") + anchors.bottom: wallpaperPathText.bottom + onClicked: { + pendingExtractJson = Theme.extractCurrentTheme(); + saveBrowserLoader.active = true; + if (saveBrowserLoader.item) + saveBrowserLoader.item.open(); + } + } } SettingsDropdownRow { @@ -3064,6 +3083,28 @@ Item { } } + LazyLoader { + id: saveBrowserLoader + active: false + + FileBrowserSurfaceModal { + id: saveBrowser + + browserTitle: I18n.tr("Save Extracted Theme", "extract theme save dialog title") + browserIcon: "download" + browserType: "default" + fileExtensions: ["*.json"] + allowStacking: true + saveMode: true + defaultFileName: "dms-extracted-theme.json" + + onFileSelected: path => { + saveExtractedTheme(pendingExtractJson, Paths.strip(path)); + close(); + } + } + } + LazyLoader { id: themeBrowserLoader active: false @@ -3079,4 +3120,26 @@ Item { if (themeBrowserLoader.item) themeBrowserLoader.item.show(); } + + FileView { + id: extractSaveFileView + blockWrites: true + preload: false + atomicWrites: true + printErrors: true + + onSaved: { + ToastService.showInfo(I18n.tr("Theme extracted to: %1", "extract theme success").arg(Paths.strip(extractSaveFileView.path))); + } + + onSaveFailed: error => { + ToastService.showError(I18n.tr("Failed to extract theme", "extract theme error")); + log.warn("Failed to write extracted theme to " + extractSaveFileView.path + ": " + error); + } + } + + function saveExtractedTheme(json, outputPath) { + extractSaveFileView.path = outputPath; + extractSaveFileView.setText(json); + } }