diff --git a/Common/StockThemes.js b/Common/StockThemes.js index 4bdb8e45..e8208110 100644 --- a/Common/StockThemes.js +++ b/Common/StockThemes.js @@ -1,12 +1,121 @@ // Stock theme definitions for DankMaterialShell // Separated from Theme.qml to keep that file clean +const CatppuccinMocha = { + surface: "#45475a", + surfaceText: "#cdd6f4", + surfaceVariant: "#45475a", + surfaceVariantText: "#a6adc8", + background: "#1e1e2e", + backgroundText: "#cdd6f4", + outline: "#6c7086", + surfaceContainer: "#313244", + surfaceContainerHigh: "#585b70" +} + +const CatppuccinLatte = { + surface: "#bcc0cc", + surfaceText: "#4c4f69", + surfaceVariant: "#bcc0cc", + surfaceVariantText: "#6c6f85", + background: "#eff1f5", + backgroundText: "#4c4f69", + outline: "#9ca0b0", + surfaceContainer: "#ccd0da", + surfaceContainerHigh: "#acb0be" +} + +const CatppuccinVariants = { + "cat-rosewater": { + name: "Rosewater", + dark: { primary: "#f5e0dc", secondary: "#f2cdcd", primaryText: "#1e1e2e", primaryContainer: "#8b6b5e", surfaceTint: "#f5e0dc" }, + light: { primary: "#dc8a78", secondary: "#dd7878", primaryText: "#ffffff", primaryContainer: "#f4d2ca", surfaceTint: "#dc8a78" } + }, + "cat-flamingo": { + name: "Flamingo", + dark: { primary: "#f2cdcd", secondary: "#f5e0dc", primaryText: "#1e1e2e", primaryContainer: "#885d62", surfaceTint: "#f2cdcd" }, + light: { primary: "#dd7878", secondary: "#dc8a78", primaryText: "#ffffff", primaryContainer: "#f4caca", surfaceTint: "#dd7878" } + }, + "cat-pink": { + name: "Pink", + dark: { primary: "#f5c2e7", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#8b537a", surfaceTint: "#f5c2e7" }, + light: { primary: "#ea76cb", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#f7c9e7", surfaceTint: "#ea76cb" } + }, + "cat-mauve": { + name: "Mauve", + dark: { primary: "#cba6f7", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#61378a", surfaceTint: "#cba6f7" }, + light: { primary: "#8839ef", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#e4d3ff", surfaceTint: "#8839ef" } + }, + "cat-red": { + name: "Red", + dark: { primary: "#f38ba8", secondary: "#eba0ac", primaryText: "#1e1e2e", primaryContainer: "#891c3b", surfaceTint: "#f38ba8" }, + light: { primary: "#d20f39", secondary: "#e64553", primaryText: "#ffffff", primaryContainer: "#f1b8c4", surfaceTint: "#d20f39" } + }, + "cat-maroon": { + name: "Maroon", + dark: { primary: "#eba0ac", secondary: "#f38ba8", primaryText: "#1e1e2e", primaryContainer: "#81313f", surfaceTint: "#eba0ac" }, + light: { primary: "#e64553", secondary: "#d20f39", primaryText: "#ffffff", primaryContainer: "#f4c3c8", surfaceTint: "#e64553" } + }, + "cat-peach": { + name: "Peach", + dark: { primary: "#fab387", secondary: "#f9e2af", primaryText: "#1e1e2e", primaryContainer: "#90441a", surfaceTint: "#fab387" }, + light: { primary: "#fe640b", secondary: "#df8e1d", primaryText: "#ffffff", primaryContainer: "#ffddcc", surfaceTint: "#fe640b" } + }, + "cat-yellow": { + name: "Yellow", + dark: { primary: "#f9e2af", secondary: "#a6e3a1", primaryText: "#1e1e2e", primaryContainer: "#8f7342", surfaceTint: "#f9e2af" }, + light: { primary: "#df8e1d", secondary: "#40a02b", primaryText: "#ffffff", primaryContainer: "#fff3cc", surfaceTint: "#df8e1d" } + }, + "cat-green": { + name: "Green", + dark: { primary: "#a6e3a1", secondary: "#94e2d5", primaryText: "#1e1e2e", primaryContainer: "#3c7534", surfaceTint: "#a6e3a1" }, + light: { primary: "#40a02b", secondary: "#179299", primaryText: "#ffffff", primaryContainer: "#d4f5d4", surfaceTint: "#40a02b" } + }, + "cat-teal": { + name: "Teal", + dark: { primary: "#94e2d5", secondary: "#89dceb", primaryText: "#1e1e2e", primaryContainer: "#2a7468", surfaceTint: "#94e2d5" }, + light: { primary: "#179299", secondary: "#04a5e5", primaryText: "#ffffff", primaryContainer: "#ccf2f2", surfaceTint: "#179299" } + }, + "cat-sky": { + name: "Sky", + dark: { primary: "#89dceb", secondary: "#74c7ec", primaryText: "#1e1e2e", primaryContainer: "#196e7e", surfaceTint: "#89dceb" }, + light: { primary: "#04a5e5", secondary: "#209fb5", primaryText: "#ffffff", primaryContainer: "#ccebff", surfaceTint: "#04a5e5" } + }, + "cat-sapphire": { + name: "Sapphire", + dark: { primary: "#74c7ec", secondary: "#89b4fa", primaryText: "#1e1e2e", primaryContainer: "#0a597f", surfaceTint: "#74c7ec" }, + light: { primary: "#209fb5", secondary: "#1e66f5", primaryText: "#ffffff", primaryContainer: "#d0f0f5", surfaceTint: "#209fb5" } + }, + "cat-blue": { + name: "Blue", + dark: { primary: "#89b4fa", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#19468d", surfaceTint: "#89b4fa" }, + light: { primary: "#1e66f5", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#ccd9ff", surfaceTint: "#1e66f5" } + }, + "cat-lavender": { + name: "Lavender", + dark: { primary: "#b4befe", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#4a5091", surfaceTint: "#b4befe" }, + light: { primary: "#7287fd", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#dde1ff", surfaceTint: "#7287fd" } + } +} + +function getCatppuccinTheme(variant, isLight = false) { + const variantData = CatppuccinVariants[variant] + if (!variantData) return null + + const baseColors = isLight ? CatppuccinLatte : CatppuccinMocha + const accentColors = isLight ? variantData.light : variantData.dark + + return Object.assign({ + name: `${variantData.name}${isLight ? ' Light' : ''}` + }, baseColors, accentColors) +} + const StockThemes = { DARK: { blue: { name: "Blue", primary: "#42a5f5", - primaryText: "#ffffff", + primaryText: "#000000", primaryContainer: "#1976d2", secondary: "#8ab4f8", surface: "#1a1c1e", @@ -23,7 +132,7 @@ const StockThemes = { deepBlue: { name: "Deep Blue", primary: "#0061a4", - primaryText: "#ffffff", + primaryText: "#000000", primaryContainer: "#004881", secondary: "#42a5f5", surface: "#1a1c1e", @@ -57,7 +166,7 @@ const StockThemes = { green: { name: "Green", primary: "#4caf50", - primaryText: "#ffffff", + primaryText: "#000000", primaryContainer: "#388e3c", secondary: "#81c995", surface: "#0f1411", @@ -74,7 +183,7 @@ const StockThemes = { orange: { name: "Orange", primary: "#ff6d00", - primaryText: "#ffffff", + primaryText: "#000000", primaryContainer: "#e65100", secondary: "#ffb74d", surface: "#1c1410", @@ -91,7 +200,7 @@ const StockThemes = { red: { name: "Red", primary: "#f44336", - primaryText: "#ffffff", + primaryText: "#000000", primaryContainer: "#d32f2f", secondary: "#f28b82", surface: "#1c1011", @@ -108,7 +217,7 @@ const StockThemes = { cyan: { name: "Cyan", primary: "#00bcd4", - primaryText: "#ffffff", + primaryText: "#000000", primaryContainer: "#0097a7", secondary: "#4dd0e1", surface: "#0f1617", @@ -125,7 +234,7 @@ const StockThemes = { pink: { name: "Pink", primary: "#e91e63", - primaryText: "#ffffff", + primaryText: "#000000", primaryContainer: "#c2185b", secondary: "#f8bbd9", surface: "#1a1014", @@ -159,7 +268,7 @@ const StockThemes = { coral: { name: "Coral", primary: "#ffb4ab", - primaryText: "#5f1412", + primaryText: "#000000", primaryContainer: "#8c1d18", secondary: "#f9dedc", surface: "#1a1110", @@ -348,6 +457,17 @@ const StockThemes = { } } +const ThemeCategories = { + GENERIC: { + name: "Generic", + variants: ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"] + }, + CATPPUCCIN: { + name: "Catppuccin", + variants: Object.keys(CatppuccinVariants) + } +} + const ThemeNames = { BLUE: "blue", DEEP_BLUE: "deepBlue", @@ -366,15 +486,30 @@ function isStockTheme(themeName) { return Object.keys(StockThemes.DARK).includes(themeName) } +function isCatppuccinVariant(themeName) { + return Object.keys(CatppuccinVariants).includes(themeName) +} + function getAvailableThemes(isLight = false) { return isLight ? StockThemes.LIGHT : StockThemes.DARK } function getThemeByName(themeName, isLight = false) { + if (isCatppuccinVariant(themeName)) { + return getCatppuccinTheme(themeName, isLight) + } const themes = getAvailableThemes(isLight) return themes[themeName] || themes.blue } function getAllThemeNames() { return Object.keys(StockThemes.DARK) +} + +function getCatppuccinVariantNames() { + return Object.keys(CatppuccinVariants) +} + +function getThemeCategories() { + return ThemeCategories } \ No newline at end of file diff --git a/Common/Theme.qml b/Common/Theme.qml index df652e3d..08c9d4a6 100644 --- a/Common/Theme.qml +++ b/Common/Theme.qml @@ -15,6 +15,7 @@ Singleton { id: root property string currentTheme: "blue" + property string currentThemeCategory: "generic" property bool isLightMode: false readonly property string dynamic: "dynamic" @@ -207,14 +208,22 @@ Singleton { function switchTheme(themeName, savePrefs = true) { if (themeName === dynamic) { currentTheme = dynamic + currentThemeCategory = "dynamic" extractColors() } else if (themeName === "custom") { currentTheme = "custom" + currentThemeCategory = "custom" if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) { loadCustomThemeFromFile(SettingsData.customThemeFile) } } else { currentTheme = themeName + // Determine category based on theme name + if (StockThemes.isCatppuccinVariant(themeName)) { + currentThemeCategory = "catppuccin" + } else { + currentThemeCategory = "generic" + } } if (savePrefs && typeof SettingsData !== "undefined") SettingsData.setTheme(currentTheme) @@ -260,6 +269,31 @@ Singleton { return StockThemes.getThemeByName(themeName, isLightMode) } + function switchThemeCategory(category, defaultTheme) { + currentThemeCategory = category + switchTheme(defaultTheme) + } + + function getCatppuccinColor(variantName) { + const catColors = { + "cat-rosewater": "#f5e0dc", "cat-flamingo": "#f2cdcd", "cat-pink": "#f5c2e7", "cat-mauve": "#cba6f7", + "cat-red": "#f38ba8", "cat-maroon": "#eba0ac", "cat-peach": "#fab387", "cat-yellow": "#f9e2af", + "cat-green": "#a6e3a1", "cat-teal": "#94e2d5", "cat-sky": "#89dceb", "cat-sapphire": "#74c7ec", + "cat-blue": "#89b4fa", "cat-lavender": "#b4befe" + } + return catColors[variantName] || "#cba6f7" + } + + function getCatppuccinVariantName(variantName) { + const catNames = { + "cat-rosewater": "Rosewater", "cat-flamingo": "Flamingo", "cat-pink": "Pink", "cat-mauve": "Mauve", + "cat-red": "Red", "cat-maroon": "Maroon", "cat-peach": "Peach", "cat-yellow": "Yellow", + "cat-green": "Green", "cat-teal": "Teal", "cat-sky": "Sky", "cat-sapphire": "Sapphire", + "cat-blue": "Blue", "cat-lavender": "Lavender" + } + return catNames[variantName] || "Unknown" + } + function loadCustomTheme(themeData) { if (themeData.dark || themeData.light) { const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark" diff --git a/Modals/Settings/SettingsSidebar.qml b/Modals/Settings/SettingsSidebar.qml index ca97a6e1..6b7660c6 100644 --- a/Modals/Settings/SettingsSidebar.qml +++ b/Modals/Settings/SettingsSidebar.qml @@ -80,7 +80,7 @@ Rectangle { width: parent.width - Theme.spacingS * 2 height: 44 radius: Theme.cornerRadius - color: isActive ? Theme.surfaceContainerHigh : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent" + color: isActive ? Theme.primaryContainer : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent" Row { anchors.left: parent.left @@ -91,14 +91,14 @@ Rectangle { DankIcon { name: modelData.icon || "" size: Theme.iconSize - 2 - color: parent.parent.isActive ? Theme.primary : Theme.surfaceText + color: parent.parent.isActive ? Theme.surfaceText : Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } StyledText { text: modelData.text || "" font.pixelSize: Theme.fontSizeMedium - color: parent.parent.isActive ? Theme.primary : Theme.surfaceText + color: parent.parent.isActive ? Theme.surfaceText : Theme.surfaceText font.weight: parent.parent.isActive ? Font.Medium : Font.Normal anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Settings/ThemeColorsTab.qml b/Modules/Settings/ThemeColorsTab.qml index f64175d0..e03556ef 100644 --- a/Modules/Settings/ThemeColorsTab.qml +++ b/Modules/Settings/ThemeColorsTab.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Effects import Quickshell import Quickshell.Io import qs.Common @@ -99,6 +100,7 @@ Item { width: parent.width spacing: Theme.spacingXL + // Theme Color StyledRect { width: parent.width @@ -142,7 +144,15 @@ Item { spacing: Theme.spacingS StyledText { - text: "Current Theme: " + (Theme.currentTheme === Theme.dynamic ? "Dynamic" : Theme.getThemeColors(Theme.currentThemeName).name) + text: { + if (Theme.currentTheme === Theme.dynamic) { + return "Current Theme: Dynamic" + } else if (Theme.currentThemeCategory === "catppuccin") { + return "Current Theme: Catppuccin " + Theme.getThemeColors(Theme.currentThemeName).name + } else { + return "Current Theme: " + Theme.getThemeColors(Theme.currentThemeName).name + } + } font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText font.weight: Font.Medium @@ -153,20 +163,10 @@ Item { text: { if (Theme.currentTheme === Theme.dynamic) return "Wallpaper-based dynamic colors" - - var descriptions = { - "blue": "Material blue inspired by modern interfaces", - "deepBlue": "Deep blue inspired by material 3", - "purple": "Rich purple tones for elegance", - "green": "Natural green for productivity", - "orange": "Energetic orange for creativity", - "red": "Bold red for impact", - "cyan": "Cool cyan for tranquility", - "pink": "Vibrant pink for expression", - "amber": "Warm amber for comfort", - "coral": "Soft coral for gentle warmth" - } - return descriptions[Theme.currentThemeName] || "Select a theme" + else if (Theme.currentThemeCategory === "catppuccin") + return "Soothing pastel theme inspired by Catppuccin" + else + return "Material Design inspired color themes" } font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText @@ -178,392 +178,477 @@ Item { } Column { - spacing: Theme.spacingS + spacing: Theme.spacingM anchors.horizontalCenter: parent.horizontalCenter - Row { - spacing: Theme.spacingM - - Repeater { - model: Theme.availableThemeNames.slice(0, 5) - - Rectangle { - 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: nameText.contentWidth + Theme.spacingS * 2 - height: nameText.contentHeight + Theme.spacingXS * 2 - color: Theme.surfaceContainer - border.color: Theme.outline - border.width: 1 - radius: Theme.cornerRadius - anchors.bottom: parent.top - anchors.bottomMargin: Theme.spacingXS - anchors.horizontalCenter: parent.horizontalCenter - visible: mouseArea.containsMouse - - StyledText { - id: nameText - - text: Theme.getThemeColors(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(themeName) - } - } - - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } - - Behavior on border.width { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } - } + DankButtonGroup { + property int currentThemeIndex: { + if (Theme.currentTheme === Theme.dynamic) return 2 + if (Theme.currentThemeName === "custom") return 3 + if (Theme.currentThemeCategory === "catppuccin") return 1 + return 0 } - } - Row { - spacing: Theme.spacingM - - Repeater { - model: Theme.availableThemeNames.slice(5, 10) - - Rectangle { - property string themeName: modelData - - width: 32 - height: 32 - radius: 16 - color: Theme.getThemeColors(themeName).primary - border.color: Theme.outline - border.width: Theme.currentThemeName === themeName ? 2 : 1 - visible: true - scale: Theme.currentThemeName === themeName ? 1.1 : 1 - - Rectangle { - width: nameText2.contentWidth + Theme.spacingS * 2 - height: nameText2.contentHeight + Theme.spacingXS * 2 - color: Theme.surfaceContainer - border.color: Theme.outline - border.width: 1 - radius: Theme.cornerRadius - anchors.bottom: parent.top - anchors.bottomMargin: Theme.spacingXS - anchors.horizontalCenter: parent.horizontalCenter - visible: mouseArea2.containsMouse - - StyledText { - id: nameText2 - - text: Theme.getThemeColors(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(themeName) - } - } - - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } - - Behavior on border.width { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } - } - } - } - - Item { - width: 1 - height: Theme.spacingM - } - - Row { + model: ["Generic", "Catppuccin", "Auto", "Custom"] + currentIndex: currentThemeIndex + selectionMode: "single" anchors.horizontalCenter: parent.horizontalCenter - spacing: Theme.spacingL - - Rectangle { - width: 120 - height: 40 - radius: 20 - color: { - if (ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus === "matugen_missing") - return Qt.rgba(Theme.error.r, - Theme.error.g, - Theme.error.b, 0.12) - else - return Qt.rgba(Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, 0.3) - } - border.color: { - if (ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus === "matugen_missing") - return Qt.rgba(Theme.error.r, - Theme.error.g, - Theme.error.b, 0.5) - else if (Theme.currentThemeName === "dynamic") - return Theme.primary - else - return Theme.outline - } - border.width: (Theme.currentThemeName === "dynamic") ? 2 : 1 - scale: (Theme.currentThemeName === "dynamic") ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1) - - Row { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - name: { - if (ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus - === "matugen_missing") - return "error" - else - return "palette" - } - size: 16 - color: { - if (ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus - === "matugen_missing") - return Theme.error - else - return Theme.surfaceText - } - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: { - if (ToastService.wallpaperErrorStatus === "error") - return "Error" - else if (ToastService.wallpaperErrorStatus - === "matugen_missing") - return "No matugen" - else - return "Auto" - } - font.pixelSize: Theme.fontSizeMedium - color: { - if (ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus - === "matugen_missing") - return Theme.error - else - return Theme.surfaceText - } - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: autoMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (ToastService.wallpaperErrorStatus === "matugen_missing") - ToastService.showError( - "matugen not found - install matugen package for dynamic theming") - else if (ToastService.wallpaperErrorStatus === "error") - ToastService.showError( - "Wallpaper processing failed - check wallpaper path") - else - Theme.switchTheme(Theme.dynamic) - } - } - - Rectangle { - width: autoTooltipText.contentWidth + Theme.spacingM * 2 - height: autoTooltipText.contentHeight + Theme.spacingS * 2 - color: Theme.surfaceContainer - border.color: Theme.outline - border.width: 1 - radius: Theme.cornerRadius - anchors.bottom: parent.top - anchors.bottomMargin: Theme.spacingS - anchors.horizontalCenter: parent.horizontalCenter - visible: autoMouseArea.containsMouse - && (Theme.currentTheme !== Theme.dynamic - || ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus - === "matugen_missing") - - StyledText { - id: autoTooltipText - - text: { + onSelectionChanged: (index, selected) => { + if (!selected) return + switch (index) { + case 0: Theme.switchThemeCategory("generic", "blue"); break + case 1: Theme.switchThemeCategory("catppuccin", "cat-mauve"); break + case 2: if (ToastService.wallpaperErrorStatus === "matugen_missing") - return "Install matugen package for dynamic themes" + ToastService.showError("matugen not found - install matugen package for dynamic theming") + else if (ToastService.wallpaperErrorStatus === "error") + ToastService.showError("Wallpaper processing failed - check wallpaper path") else - return "Dynamic wallpaper-based colors" - } - font.pixelSize: Theme.fontSizeSmall - color: (ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus - === "matugen_missing") ? Theme.error : Theme.surfaceText - anchors.centerIn: parent - wrapMode: Text.WordWrap - width: Math.min(implicitWidth, 250) - horizontalAlignment: Text.AlignHCenter - } - } - - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.mediumDuration - easing.type: Theme.standardEasing - } - } - - Behavior on border.color { - ColorAnimation { - duration: Theme.mediumDuration - easing.type: Theme.standardEasing + Theme.switchTheme(Theme.dynamic) + break + case 3: + if (Theme.currentThemeName !== "custom") { + Theme.switchTheme("custom") + } + break } } } - Rectangle { - width: 120 - height: 40 - radius: 20 - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) - border.color: (Theme.currentThemeName === "custom") ? Theme.primary : Theme.outline - border.width: (Theme.currentThemeName === "custom") ? 2 : 1 - scale: (Theme.currentThemeName === "custom") ? 1.1 : (customMouseArea.containsMouse ? 1.02 : 1) + Column { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + visible: Theme.currentThemeCategory === "generic" && Theme.currentTheme !== Theme.dynamic && Theme.currentThemeName !== "custom" Row { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - name: "folder_open" - size: 16 - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "Custom" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: customMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - fileBrowserModal.open() - } - } - - Rectangle { - width: customTooltipText.contentWidth + Theme.spacingM * 2 - height: customTooltipText.contentHeight + Theme.spacingS * 2 - color: Theme.surfaceContainer - border.color: Theme.outline - border.width: 1 - radius: Theme.cornerRadius - anchors.bottom: parent.top - anchors.bottomMargin: Theme.spacingS + spacing: Theme.spacingM anchors.horizontalCenter: parent.horizontalCenter - visible: customMouseArea.containsMouse - StyledText { - id: customTooltipText - text: { - if (Theme.currentThemeName === "custom") - return SettingsData.customThemeFile || "Custom theme loaded" - else - return "Load custom theme from JSON file" + Repeater { + model: ["blue", "deepBlue", "purple", "green", "orange"] + + Rectangle { + 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: nameText.contentWidth + Theme.spacingS * 2 + height: nameText.contentHeight + Theme.spacingXS * 2 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + visible: mouseArea.containsMouse + + StyledText { + id: nameText + text: Theme.getThemeColors(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(themeName) + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } } - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - anchors.centerIn: parent - wrapMode: Text.WordWrap - width: Math.min(implicitWidth, 250) - horizontalAlignment: Text.AlignHCenter } } - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } + Row { + spacing: Theme.spacingM + anchors.horizontalCenter: parent.horizontalCenter - Behavior on border.width { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing + Repeater { + model: ["red", "cyan", "pink", "amber", "coral"] + + Rectangle { + 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 + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + visible: mouseArea2.containsMouse + + StyledText { + id: nameText2 + text: Theme.getThemeColors(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(themeName) + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + } + + Column { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + visible: Theme.currentThemeCategory === "catppuccin" && Theme.currentTheme !== Theme.dynamic && Theme.currentThemeName !== "custom" + + Row { + spacing: Theme.spacingM + anchors.horizontalCenter: parent.horizontalCenter + + Repeater { + model: ["cat-rosewater", "cat-flamingo", "cat-pink", "cat-mauve", "cat-red", "cat-maroon", "cat-peach"] + + Rectangle { + 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: nameTextCat.contentWidth + Theme.spacingS * 2 + height: nameTextCat.contentHeight + Theme.spacingXS * 2 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + visible: mouseAreaCat.containsMouse + + StyledText { + id: nameTextCat + text: Theme.getCatppuccinVariantName(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(themeName) + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + + Row { + spacing: Theme.spacingM + anchors.horizontalCenter: parent.horizontalCenter + + Repeater { + model: ["cat-yellow", "cat-green", "cat-teal", "cat-sky", "cat-sapphire", "cat-blue", "cat-lavender"] + + Rectangle { + 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 + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + visible: mouseAreaCat2.containsMouse + + StyledText { + id: nameTextCat2 + text: Theme.getCatppuccinVariantName(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(themeName) + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: Theme.currentTheme === Theme.dynamic + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledRect { + width: 120 + height: 90 + radius: Theme.cornerRadius + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + + CachingImage { + anchors.fill: parent + anchors.margins: 1 + source: Theme.wallpaperPath ? "file://" + Theme.wallpaperPath : "" + fillMode: Image.PreserveAspectCrop + visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#") + layer.enabled: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: autoWallpaperMask + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#") ? Theme.wallpaperPath : "transparent" + visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#") + } + + Rectangle { + id: autoWallpaperMask + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: "black" + visible: false + layer.enabled: true + } + + DankIcon { + anchors.centerIn: parent + name: { + if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") + return "error" + else + return "palette" + } + size: Theme.iconSizeLarge + color: { + if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") + return Theme.error + else + return Theme.surfaceVariantText + } + visible: !Theme.wallpaperPath + } + } + + Column { + width: parent.width - 120 - Theme.spacingM + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: { + if (ToastService.wallpaperErrorStatus === "error") + return "Wallpaper Error" + else if (ToastService.wallpaperErrorStatus === "matugen_missing") + return "Matugen Missing" + else if (Theme.wallpaperPath) + return Theme.wallpaperPath.split('/').pop() + else + return "No wallpaper selected" + } + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } + + StyledText { + text: { + if (ToastService.wallpaperErrorStatus === "error") + return "Wallpaper processing failed" + else if (ToastService.wallpaperErrorStatus === "matugen_missing") + return "Install matugen package for dynamic theming" + else if (Theme.wallpaperPath) + return Theme.wallpaperPath + else + return "Dynamic colors from wallpaper" + } + font.pixelSize: Theme.fontSizeSmall + color: { + if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") + return Theme.error + else + return Theme.surfaceVariantText + } + elide: Text.ElideMiddle + maximumLineCount: 2 + width: parent.width + wrapMode: Text.WordWrap + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: Theme.currentThemeName === "custom" + + Row { + width: parent.width + spacing: Theme.spacingM + + DankActionButton { + buttonSize: 48 + iconName: "folder_open" + iconSize: Theme.iconSize + backgroundColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + iconColor: Theme.primary + onClicked: fileBrowserModal.open() + } + + Column { + width: parent.width - 48 - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: SettingsData.customThemeFile ? SettingsData.customThemeFile.split('/').pop() : "No custom theme file" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } + + StyledText { + text: SettingsData.customThemeFile || "Click to select a custom theme JSON file" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } } } } - } // Close Row } } } diff --git a/Widgets/DankButtonGroup.qml b/Widgets/DankButtonGroup.qml new file mode 100644 index 00000000..7a12cf37 --- /dev/null +++ b/Widgets/DankButtonGroup.qml @@ -0,0 +1,185 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Row { + id: root + + property var model: [] + property int currentIndex: -1 + property string selectionMode: "single" + property bool multiSelect: selectionMode === "multi" + + signal selectionChanged(int index, bool selected) + + spacing: Theme.spacingXS + + function isSelected(index) { + if (multiSelect) { + return repeater.itemAt(index)?.selected || false + } + return index === currentIndex + } + + function selectItem(index) { + if (multiSelect) { + const item = repeater.itemAt(index) + if (item) { + item.selected = !item.selected + selectionChanged(index, item.selected) + } + } else { + const oldIndex = currentIndex + currentIndex = index + selectionChanged(index, true) + if (oldIndex !== index && oldIndex >= 0) { + selectionChanged(oldIndex, false) + } + } + } + + Repeater { + id: repeater + model: root.model + + delegate: Rectangle { + id: segment + + property bool selected: multiSelect ? false : (index === root.currentIndex) + property bool hovered: mouseArea.containsMouse + property bool pressed: mouseArea.pressed + property bool isFirst: index === 0 + property bool isLast: index === repeater.count - 1 + property bool prevSelected: index > 0 ? root.isSelected(index - 1) : false + property bool nextSelected: index < repeater.count - 1 ? root.isSelected(index + 1) : false + + width: Math.max(contentItem.implicitWidth + Theme.spacingL * 2, 64) + (selected ? 4 : 0) + height: 40 + + color: selected ? Theme.primaryContainer : Theme.primary + border.color: "transparent" + border.width: 0 + + topLeftRadius: (isFirst || selected) ? Theme.cornerRadius : 4 + bottomLeftRadius: (isFirst || selected) ? Theme.cornerRadius : 4 + topRightRadius: (isLast || selected) ? Theme.cornerRadius : 4 + bottomRightRadius: (isLast || selected) ? Theme.cornerRadius : 4 + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on topLeftRadius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on topRightRadius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on bottomLeftRadius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on bottomRightRadius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Rectangle { + id: stateLayer + anchors.fill: parent + topLeftRadius: parent.topLeftRadius + bottomLeftRadius: parent.bottomLeftRadius + topRightRadius: parent.topRightRadius + bottomRightRadius: parent.bottomRightRadius + color: { + if (pressed) return selected ? Theme.primaryPressed : Theme.surfacePressed + if (hovered) return selected ? Theme.primaryHover : Theme.surfaceHover + return "transparent" + } + + Behavior on color { + ColorAnimation { + duration: Theme.shorterDuration + easing.type: Theme.standardEasing + } + } + } + + Item { + id: contentItem + anchors.centerIn: parent + implicitWidth: contentRow.implicitWidth + implicitHeight: contentRow.implicitHeight + + Row { + id: contentRow + spacing: Theme.spacingS + + DankIcon { + id: checkIcon + name: "check" + size: Theme.iconSizeSmall + color: segment.selected ? Theme.surfaceText : Theme.primaryText + visible: segment.selected + opacity: segment.selected ? 1 : 0 + scale: segment.selected ? 1 : 0.6 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + + StyledText { + id: buttonText + text: typeof modelData === "string" ? modelData : modelData.text || "" + font.pixelSize: Theme.fontSizeMedium + font.weight: segment.selected ? Font.Medium : Font.Normal + color: segment.selected ? Theme.surfaceText : Theme.primaryText + verticalAlignment: Text.AlignVCenter + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.selectItem(index) + } + } + } +} \ No newline at end of file diff --git a/Widgets/DankToggle.qml b/Widgets/DankToggle.qml index 4b1ccfb2..ceea2aaa 100644 --- a/Widgets/DankToggle.qml +++ b/Widgets/DankToggle.qml @@ -5,6 +5,7 @@ import qs.Widgets Item { id: toggle + // API property bool checked: false property bool enabled: true property bool toggling: false @@ -17,13 +18,15 @@ Item { readonly property bool showText: text && !hideText - width: showText ? parent.width : 48 - height: showText ? 60 : 24 + readonly property int trackWidth: 52 + readonly property int trackHeight: 30 + readonly property int insetCircle: 24 + + width: showText ? parent.width : trackWidth + height: showText ? 60 : trackHeight function handleClick() { - if (!enabled) { - return - } + if (!enabled) return checked = !checked clicked() toggled(checked) @@ -31,7 +34,6 @@ Item { StyledRect { id: background - anchors.fill: parent radius: showText ? Theme.cornerRadius : 0 color: showText ? Theme.surfaceHover : "transparent" @@ -80,26 +82,35 @@ Item { StyledRect { id: toggleTrack - width: text ? 48 : parent.width - height: text ? 24 : parent.height + width: showText ? trackWidth : Math.max(parent.width, trackWidth) + height: showText ? trackHeight : Math.max(parent.height, trackHeight) anchors.right: parent.right - anchors.rightMargin: text ? Theme.spacingM : 0 + anchors.rightMargin: showText ? Theme.spacingM : 0 anchors.verticalCenter: parent.verticalCenter radius: height / 2 + color: (checked && enabled) ? Theme.primary : Theme.surfaceVariantAlpha opacity: toggling ? 0.6 : (enabled ? 1 : 0.4) - StyledRect { - id: toggleHandle + border.color: (!checked || !enabled) ? Theme.outline : "transparent" - width: 20 - height: 20 - radius: 10 - color: Theme.surface + readonly property int pad: Math.round((height - thumb.width) / 2) + readonly property int edgeLeft: pad + readonly property int edgeRight: width - thumb.width - pad + + StyledRect { + id: thumb + + width: (checked && enabled) ? insetCircle : insetCircle - 4 + height: (checked && enabled) ? insetCircle : insetCircle - 4 + radius: width / 2 anchors.verticalCenter: parent.verticalCenter - x: (checked && enabled) ? parent.width - width - 2 : 2 - border.color: Qt.rgba(0, 0, 0, 0.1) - border.width: 1 + + color: (checked && enabled) ? Theme.surface : Theme.outline + border.color: (checked && enabled) ? Theme.outline : Theme.outline + border.width: (checked && enabled) ? 1 : 2 + + x: (checked && enabled) ? toggleTrack.edgeRight : toggleTrack.edgeLeft Behavior on x { NumberAnimation { @@ -108,6 +119,48 @@ Item { easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } } + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + Behavior on border.width { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + DankIcon { + id: checkIcon + anchors.centerIn: parent + name: "check" + size: 20 + color: Theme.surfaceText + filled: true + opacity: checked && enabled ? 1 : 0 + scale: checked && enabled ? 1 : 0.6 + + Behavior on opacity { + NumberAnimation { + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasized + } + } + Behavior on scale { + NumberAnimation { + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasized + } + } + } } StateLayer {