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"
/>
+