1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-08 06:25:37 -05:00

Add catpuccin theme + material 3 components

- Update DankToggle to be more representative of m3 design
- Add DankButtonGroup to represent a m3 button group
This commit is contained in:
bbedward
2025-09-17 14:54:34 -04:00
parent 102cc3c0b6
commit 6c81aa0908
6 changed files with 899 additions and 407 deletions

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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
}
}
}

185
Widgets/DankButtonGroup.qml Normal file
View File

@@ -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)
}
}
}
}

View File

@@ -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 {