diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 6f1dfd7682..32f5ba1517 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -705,6 +705,8 @@ settings-interface-appearance-decorations = Use the system native decorations settings-interface-appearance-decorations-description = This will not render the top bar of the interface and will use the operating system's instead. settings-interface-appearance-decorations-label = Use native decorations +settings-interface-appearance-hue = Hue + ## Notification settings settings-interface-notifications = Notifications settings-general-interface-serial_detection = Serial device detection diff --git a/gui/src/AppLayout.tsx b/gui/src/AppLayout.tsx index 6e0aee1c2f..1578f9d9be 100644 --- a/gui/src/AppLayout.tsx +++ b/gui/src/AppLayout.tsx @@ -1,6 +1,7 @@ import { useLayoutEffect } from 'react'; import { useConfig } from './hooks/config'; import { Outlet, useNavigate } from 'react-router-dom'; +import { applyColors } from './components/commons/CustomThemeColors'; export function AppLayout() { const { config } = useConfig(); @@ -10,6 +11,7 @@ export function AppLayout() { if (!config) return; if (config.theme !== undefined) { document.documentElement.dataset.theme = config.theme; + applyColors(config.theme, config.customHue); } if (config.fonts !== undefined) { diff --git a/gui/src/components/commons/CustomThemeColors.tsx b/gui/src/components/commons/CustomThemeColors.tsx new file mode 100644 index 0000000000..71e8c0790b --- /dev/null +++ b/gui/src/components/commons/CustomThemeColors.tsx @@ -0,0 +1,117 @@ +function hsvToRGB(h: number, s: number, v: number): [number, number, number] { + let r = 0, + g = 0, + b = 0; + const i = Math.floor(h * 6); + const f = h * 6 - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + switch (i % 6) { + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + break; + } + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; +} + +export function applyColors(theme: string, hue: number) { + if (theme == 'custom-bright') { + document.documentElement.style.setProperty( + '--background-20', + hsvToRGB(hue, 0.06, 0.91).join(',') + ); + document.documentElement.style.setProperty( + '--background-30', + hsvToRGB(hue, 0.06, 0.83).join(',') + ); + document.documentElement.style.setProperty( + '--background-40', + hsvToRGB(hue, 0.1, 0.61).join(',') + ); + document.documentElement.style.setProperty( + '--background-50', + hsvToRGB(hue, 0.21, 0.38).join(',') + ); + document.documentElement.style.setProperty( + '--background-60', + hsvToRGB(hue, 0.25, 0.28).join(',') + ); + document.documentElement.style.setProperty( + '--background-70', + hsvToRGB(hue, 0.27, 0.22).join(',') + ); + document.documentElement.style.setProperty( + '--background-80', + hsvToRGB(hue, 0.29, 0.15).join(',') + ); + } else { + document.documentElement.style.setProperty('--background-20', null); + document.documentElement.style.setProperty('--background-30', null); + document.documentElement.style.setProperty('--background-40', null); + document.documentElement.style.setProperty('--background-50', null); + document.documentElement.style.setProperty('--background-60', null); + document.documentElement.style.setProperty('--background-70', null); + document.documentElement.style.setProperty('--background-80', null); + } + if (theme == 'custom-bright' || theme == 'custom-dark') { + document.documentElement.style.setProperty( + '--accent-background-10', + hsvToRGB(hue, 0.2, 1).join(',') + ); + document.documentElement.style.setProperty( + '--accent-background-20', + hsvToRGB(hue, 0.51, 0.92).join(',') + ); + document.documentElement.style.setProperty( + '--accent-background-30', + hsvToRGB(hue, 0.62, 0.72).join(',') + ); + document.documentElement.style.setProperty( + '--accent-background-40', + hsvToRGB(hue, 0.62, 0.56).join(',') + ); + document.documentElement.style.setProperty( + '--accent-background-50', + hsvToRGB(hue, 0.7, 0.36).join(',') + ); + document.documentElement.style.setProperty( + '--window-icon-stroke', + hsvToRGB(hue, 0.43, 0.92).join(',') + ); + } else { + document.documentElement.style.setProperty('--accent-background-10', null); + document.documentElement.style.setProperty('--accent-background-20', null); + document.documentElement.style.setProperty('--accent-background-30', null); + document.documentElement.style.setProperty('--accent-background-40', null); + document.documentElement.style.setProperty('--accent-background-50', null); + document.documentElement.style.setProperty('--window-icon-stroke', null); + } +} diff --git a/gui/src/components/settings/pages/InterfaceSettings.tsx b/gui/src/components/settings/pages/InterfaceSettings.tsx index 0f870746ef..1efa68c863 100644 --- a/gui/src/components/settings/pages/InterfaceSettings.tsx +++ b/gui/src/components/settings/pages/InterfaceSettings.tsx @@ -28,6 +28,7 @@ interface InterfaceSettingsForm { textSize: number; fonts: string; decorations: boolean; + customHue: number; }; behavior: { devmode: boolean; @@ -55,6 +56,7 @@ export function InterfaceSettings() { textSize: config?.textSize ?? defaultConfig.textSize, fonts: config?.fonts.join(',') ?? defaultConfig.fonts.join(','), decorations: config?.decorations ?? defaultConfig.decorations, + customHue: config?.customHue ?? defaultConfig.customHue, }, notifications: { watchNewDevices: @@ -112,6 +114,7 @@ export function InterfaceSettings() { fonts: values.appearance.fonts.split(','), textSize: values.appearance.textSize, decorations: values.appearance.decorations, + customHue: values.appearance.customHue, useTray: values.behavior.useTray, discordPresence: values.behavior.discordPresence, @@ -470,9 +473,45 @@ export function InterfaceSettings() { value={'snep'} colors="!bg-snep" /> + + +
+ + {l10n.getString('settings-interface-appearance-hue')} + +
+
+ +
+ {l10n.getString('settings-interface-appearance-font')} diff --git a/gui/src/hooks/config.ts b/gui/src/hooks/config.ts index 68b921d691..2930d322f4 100644 --- a/gui/src/hooks/config.ts +++ b/gui/src/hooks/config.ts @@ -50,6 +50,7 @@ export interface Config { homeLayout: 'default' | 'table'; skeletonPreview: boolean; lastUsedProportions: 'manual' | 'autobone' | 'scaled' | null; + customHue: number; } export interface ConfigContext { @@ -82,6 +83,7 @@ export const defaultConfig: Config = { homeLayout: 'default', skeletonPreview: true, lastUsedProportions: null, + customHue: 0.8, }; interface CrossStorage { diff --git a/gui/src/index.scss b/gui/src/index.scss index 8a422d013b..14186dda4d 100644 --- a/gui/src/index.scss +++ b/gui/src/index.scss @@ -343,6 +343,37 @@ body { --default-color: 255, 255, 255; } +:root[data-theme='custom-bright'] { + --background-10: 255, 255, 255; + --background-90: 0, 0, 0; + + --success: 139, 223, 35; + --warning: 255, 187, 62; + --critical: 223, 54, 84; + --special: 230, 0, 230; + + --default-color: 255, 255, 255; +} + +:root[data-theme='custom-dark'] { + --background-10: 255, 255, 255; + --background-20: 230, 230, 230; + --background-30: 200, 200, 200; + --background-40: 120, 120, 120; + --background-50: 90, 90, 90; + --background-60: 60, 60, 60; + --background-70: 30, 30, 30; + --background-80: 15, 15, 15; + --background-90: 0, 0, 0; + + --success: 139, 223, 35; + --warning: 255, 187, 62; + --critical: 223, 54, 84; + --special: 230, 0, 230; + + --default-color: 255, 255, 255; +} + #root { height: 100%; } diff --git a/gui/tailwind.config.ts b/gui/tailwind.config.ts index 07004e88cd..6f129a8a35 100644 --- a/gui/tailwind.config.ts +++ b/gui/tailwind.config.ts @@ -295,6 +295,8 @@ const config = { 'trans-flag': `linear-gradient(135deg, ${colors['trans-blue'][800]} 40%, ${colors['trans-blue'][700]} 40% 70%, ${colors['trans-blue'][600]} 70% 100%)`, 'asexual-flag': `linear-gradient(135deg, ${colors['asexual'][100]} 30%, ${colors['asexual'][200]} 30% 50%, ${colors['asexual'][300]} 50% 70%, ${colors['asexual'][400]} 70% 100%)`, 'snep': `linear-gradient(135deg, ${colors['snep'][100]} 40%, ${colors['snep'][200]} 40% 70%, ${colors['snep'][300]} 70% 100%)`, + 'custom-bright': `linear-gradient(135deg, ${colors['red-accent'][100]} 30%, ${colors['orange-accent'][100]} 30% 43.3%, ${colors['yellow-accent'][100]} 43.3% 56.7%, ${colors['green-accent'][100]} 56.7% 70%, ${colors['blue-gray'][300]} 70% 100%)`, + 'custom-dark': `linear-gradient(135deg, ${colors['red-accent'][700]} 30%, ${colors['orange-accent'][700]} 30% 43.3%, ${colors['yellow-accent'][700]} 43.3% 56.7%, ${colors['green-accent'][700]} 56.7% 70%, ${colors['blue-gray'][700]} 70% 100%)`, }, animation: { 'spin-ccw': 'spin-ccw 1s linear infinite',