From 93791aea152ab93abcde227d6f291775cc3b0786 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 4 Mar 2026 20:58:30 -0800 Subject: [PATCH] add setting to override theme primary shade (#1791) --- src/i18n/locales/en.json | 4 ++ .../analytics/hooks/use-app-tracker.ts | 2 + .../components/general/theme-settings.tsx | 46 +++++++++++++++++++ src/renderer/store/settings.store.ts | 6 +++ src/renderer/themes/use-app-theme.ts | 26 +++++++++-- 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f375e06e1..2ca1a0cec 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -724,6 +724,10 @@ "accentColor": "accent color", "useThemeAccentColor": "use theme accent color", "useThemeAccentColor_description": "use the primary color defined in the selected theme instead of the custom accent color", + "useThemePrimaryShade": "use theme primary shade", + "useThemePrimaryShade_description": "use the primary shade defined in the selected theme for primary color variants", + "primaryShade": "primary shade", + "primaryShade_description": "override the primary shade (0–9) used for buttons, links, and other primary-colored elements", "albumBackground_description": "adds a background image for album pages containing the album art", "albumBackground": "album background image", "albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image", diff --git a/src/renderer/features/analytics/hooks/use-app-tracker.ts b/src/renderer/features/analytics/hooks/use-app-tracker.ts index fe445c20d..96bd64a63 100644 --- a/src/renderer/features/analytics/hooks/use-app-tracker.ts +++ b/src/renderer/features/analytics/hooks/use-app-tracker.ts @@ -103,6 +103,7 @@ type SettingsProperties = { 'settings.themeLight': string; 'settings.tray': boolean; 'settings.useThemeAccentColor': boolean; + 'settings.useThemePrimaryShade': boolean; 'settings.windowBarStyle': Platform; 'settings.zoomFactor': number; }; @@ -192,6 +193,7 @@ const getSettingsProperties = (): SettingsProperties => { 'settings.themeLight': settings.general.themeLight, 'settings.tray': ignoreWeb(settings.window.tray), 'settings.useThemeAccentColor': settings.general.useThemeAccentColor, + 'settings.useThemePrimaryShade': settings.general.useThemePrimaryShade, 'settings.windowBarStyle': ignoreWeb(settings.window.windowBarStyle), 'settings.zoomFactor': ignoreWeb(settings.general.zoomFactor), } as any; diff --git a/src/renderer/features/settings/components/general/theme-settings.tsx b/src/renderer/features/settings/components/general/theme-settings.tsx index da0308821..2d38658df 100644 --- a/src/renderer/features/settings/components/general/theme-settings.tsx +++ b/src/renderer/features/settings/components/general/theme-settings.tsx @@ -13,6 +13,7 @@ import { THEME_DATA, useSetColorScheme } from '/@/renderer/themes/use-app-theme' import { ColorInput } from '/@/shared/components/color-input/color-input'; import { Group } from '/@/shared/components/group/group'; import { Select } from '/@/shared/components/select/select'; +import { Slider } from '/@/shared/components/slider/slider'; import { Stack } from '/@/shared/components/stack/stack'; import { Switch } from '/@/shared/components/switch/switch'; import { getAppTheme } from '/@/shared/themes/app-theme'; @@ -253,6 +254,51 @@ export const ThemeSettings = memo(() => { }), title: t('setting.accentColor', { postProcess: 'sentenceCase' }), }, + { + control: ( + { + setSettings({ + general: { + useThemePrimaryShade: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.useThemePrimaryShade', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: false, + title: t('setting.useThemePrimaryShade', { postProcess: 'sentenceCase' }), + }, + { + control: ( + value} + max={9} + min={0} + onChangeEnd={(value) => { + setSettings({ + general: { + primaryShade: value, + }, + }); + }} + step={1} + w={120} + /> + ), + description: t('setting.primaryShade', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: settings.useThemePrimaryShade, + title: t('setting.primaryShade', { postProcess: 'sentenceCase' }), + }, ]; return ( diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index cb5fb7bbe..d7fce37df 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -462,6 +462,7 @@ export const GeneralSettingsSchema = z.object({ playerbarOpenDrawer: z.boolean(), playerbarSlider: PlayerbarSliderSchema, playlistTarget: PlaylistTargetSchema, + primaryShade: z.number().min(0).max(9), resume: z.boolean(), showLyricsInSidebar: z.boolean(), showRatings: z.boolean(), @@ -479,6 +480,7 @@ export const GeneralSettingsSchema = z.object({ themeDark: z.nativeEnum(AppTheme), themeLight: z.nativeEnum(AppTheme), useThemeAccentColor: z.boolean(), + useThemePrimaryShade: z.boolean(), volumeWheelStep: z.number(), volumeWidth: z.number(), zoomFactor: z.number(), @@ -1051,6 +1053,7 @@ const initialState: SettingsState = { type: PlayerbarSliderType.SLIDER, }, playlistTarget: PlaylistTarget.TRACK, + primaryShade: 6, resume: true, showLyricsInSidebar: true, showRatings: true, @@ -1072,6 +1075,7 @@ const initialState: SettingsState = { themeDark: AppTheme.DEFAULT_DARK, themeLight: AppTheme.DEFAULT_LIGHT, useThemeAccentColor: false, + useThemePrimaryShade: true, volumeWheelStep: 5, volumeWidth: 70, zoomFactor: 100, @@ -2371,10 +2375,12 @@ export const useThemeSettings = () => useSettingsStore( (state) => ({ followSystemTheme: state.general.followSystemTheme, + primaryShade: state.general.primaryShade, theme: state.general.theme, themeDark: state.general.themeDark, themeLight: state.general.themeLight, useThemeAccentColor: state.general.useThemeAccentColor, + useThemePrimaryShade: state.general.useThemePrimaryShade, }), shallow, ); diff --git a/src/renderer/themes/use-app-theme.ts b/src/renderer/themes/use-app-theme.ts index b033c10d7..6fcdb9bf9 100644 --- a/src/renderer/themes/use-app-theme.ts +++ b/src/renderer/themes/use-app-theme.ts @@ -52,7 +52,7 @@ export const useAppTheme = (overrideTheme?: AppTheme) => { const themeInlineStylesRef = useRef(null); const getCurrentTheme = () => window.matchMedia('(prefers-color-scheme: dark)').matches; const [isDarkTheme, setIsDarkTheme] = useState(getCurrentTheme()); - const { followSystemTheme, theme, themeDark, themeLight, useThemeAccentColor } = + const { followSystemTheme, primaryShade, theme, themeDark, themeLight, useThemeAccentColor, useThemePrimaryShade } = useThemeSettings(); const mqListener = (e: any) => { @@ -144,14 +144,23 @@ export const useAppTheme = (overrideTheme?: AppTheme) => { ? themeProperties.colors?.primary || themeProperties.colors?.['state-info'] || accent : accent; + // Use theme's primary shade if useThemePrimaryShade is enabled, otherwise use slider value + const effectivePrimaryShade = useThemePrimaryShade + ? themeProperties.mantineOverride?.primaryShade + : { dark: primaryShade, light: primaryShade }; + return { ...themeProperties, colors: { ...themeProperties.colors, primary: primaryColor, }, + mantineOverride: { + ...themeProperties.mantineOverride, + ...(effectivePrimaryShade != null && { primaryShade: effectivePrimaryShade }), + }, }; - }, [accent, selectedTheme, useThemeAccentColor]); + }, [accent, primaryShade, selectedTheme, useThemeAccentColor, useThemePrimaryShade]); useEffect(() => { const root = document.documentElement; @@ -241,7 +250,7 @@ export const useAppThemeColors = () => { const accent = useAccent(); const getCurrentTheme = () => window.matchMedia('(prefers-color-scheme: dark)').matches; const [isDarkTheme] = useState(getCurrentTheme()); - const { followSystemTheme, theme, themeDark, themeLight, useThemeAccentColor } = + const { followSystemTheme, primaryShade, theme, themeDark, themeLight, useThemeAccentColor, useThemePrimaryShade } = useThemeSettings(); const getSelectedTheme = () => { @@ -262,14 +271,23 @@ export const useAppThemeColors = () => { ? themeProperties.colors?.primary || themeProperties.colors?.['state-info'] || accent : accent; + // Use theme's primary shade if useThemePrimaryShade is enabled, otherwise use slider value + const effectivePrimaryShade = useThemePrimaryShade + ? themeProperties.mantineOverride?.primaryShade + : { dark: primaryShade, light: primaryShade }; + return { ...themeProperties, colors: { ...themeProperties.colors, primary: primaryColor, }, + mantineOverride: { + ...themeProperties.mantineOverride, + ...(effectivePrimaryShade != null && { primaryShade: effectivePrimaryShade }), + }, }; - }, [accent, selectedTheme, useThemeAccentColor]); + }, [accent, primaryShade, selectedTheme, useThemeAccentColor, useThemePrimaryShade]); const themeVars = useMemo(() => { return Object.entries(appTheme?.app ?? {})