diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx
index ede6a307f..8a6d20807 100644
--- a/src/components/AppShelf/AppShelf.tsx
+++ b/src/components/AppShelf/AppShelf.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { Accordion, createStyles, Grid, Group } from '@mantine/core';
+import { Accordion, createStyles, Grid, Group, Paper, useMantineColorScheme } from '@mantine/core';
import {
closestCenter,
DndContext,
@@ -42,6 +42,7 @@ const AppShelf = (props: any) => {
});
const [activeId, setActiveId] = useState(null);
const { config, setConfig } = useConfig();
+ const { colorScheme } = useMantineColorScheme();
const sensors = useSensors(
useSensor(TouchSensor, {
@@ -164,7 +165,16 @@ const AppShelf = (props: any) => {
) : null}
-
+
+
+
diff --git a/src/components/AppShelf/AppShelfItem.tsx b/src/components/AppShelf/AppShelfItem.tsx
index 9af38f6d3..8ad389a19 100644
--- a/src/components/AppShelf/AppShelfItem.tsx
+++ b/src/components/AppShelf/AppShelfItem.tsx
@@ -1,4 +1,13 @@
-import { Text, Card, Anchor, AspectRatio, Image, Center, createStyles } from '@mantine/core';
+import {
+ Text,
+ Card,
+ Anchor,
+ AspectRatio,
+ Image,
+ Center,
+ createStyles,
+ useMantineColorScheme,
+} from '@mantine/core';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { useSortable } from '@dnd-kit/sortable';
@@ -6,6 +15,7 @@ import { CSS } from '@dnd-kit/utilities';
import { serviceItem } from '../../tools/types';
import PingComponent from '../modules/ping/PingModule';
import AppShelfMenu from './AppShelfMenu';
+import { useConfig } from '../../tools/state';
const useStyles = createStyles((theme) => ({
item: {
@@ -41,6 +51,8 @@ export function SortableAppShelfItem(props: any) {
export function AppShelfItem(props: any) {
const { service }: { service: serviceItem } = props;
const [hovering, setHovering] = useState(false);
+ const { config } = useConfig();
+ const { colorScheme } = useMantineColorScheme();
const { classes } = useStyles();
return (
-
+
({
root: {
@@ -29,6 +30,7 @@ const useStyles = createStyles((theme) => ({
}));
export function ColorSchemeSwitch() {
+ const { config } = useConfig();
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const { classes, cx } = useStyles();
diff --git a/src/components/Settings/AdvancedSettings.tsx b/src/components/Settings/AdvancedSettings.tsx
index 7b678f10f..b00a11933 100644
--- a/src/components/Settings/AdvancedSettings.tsx
+++ b/src/components/Settings/AdvancedSettings.tsx
@@ -1,6 +1,9 @@
import { TextInput, Group, Button } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useConfig } from '../../tools/state';
+import { ColorSelector } from './ColorSelector';
+import { OpacitySelector } from './OpacitySelector';
+import { ShadeSelector } from './ShadeSelector';
export default function TitleChanger() {
const { config, setConfig } = useConfig();
@@ -10,10 +13,16 @@ export default function TitleChanger() {
title: config.settings.title,
logo: config.settings.logo,
favicon: config.settings.favicon,
+ background: config.settings.background,
},
});
- const saveChanges = (values: { title?: string; logo?: string; favicon?: string }) => {
+ const saveChanges = (values: {
+ title?: string;
+ logo?: string;
+ favicon?: string;
+ background?: string;
+ }) => {
setConfig({
...config,
settings: {
@@ -21,6 +30,7 @@ export default function TitleChanger() {
title: values.title,
logo: values.logo,
favicon: values.favicon,
+ background: values.background,
},
});
};
@@ -36,9 +46,18 @@ export default function TitleChanger() {
placeholder="/favicon.svg"
{...form.getInputProps('favicon')}
/>
+
+
+
+
+
);
}
diff --git a/src/components/Settings/ColorSelector.tsx b/src/components/Settings/ColorSelector.tsx
new file mode 100644
index 000000000..e7f175b3d
--- /dev/null
+++ b/src/components/Settings/ColorSelector.tsx
@@ -0,0 +1,96 @@
+import React, { useState } from 'react';
+import { ColorSwatch, Group, Popover, Text, useMantineTheme } from '@mantine/core';
+import { useConfig } from '../../tools/state';
+import { useColorTheme } from '../../tools/color';
+
+interface ColorControlProps {
+ type: string;
+}
+
+export function ColorSelector({ type }: ColorControlProps) {
+ const { config, setConfig } = useConfig();
+ const [opened, setOpened] = useState(false);
+
+ const { primaryColor, secondaryColor, setPrimaryColor, setSecondaryColor } = useColorTheme();
+
+ const theme = useMantineTheme();
+ const colors = Object.keys(theme.colors).map((color) => ({
+ swatch: theme.colors[color][6],
+ color,
+ }));
+
+ const configColor = type === 'primary' ? primaryColor : secondaryColor;
+
+ const setConfigColor = (color: string) => {
+ if (type === 'primary') {
+ setPrimaryColor(color);
+ setConfig({
+ ...config,
+ settings: {
+ ...config.settings,
+ primaryColor: color,
+ },
+ });
+ } else {
+ setSecondaryColor(color);
+ setConfig({
+ ...config,
+ settings: {
+ ...config.settings,
+ secondaryColor: color,
+ },
+ });
+ }
+ };
+
+ const swatches = colors.map(({ color, swatch }) => (
+ setConfigColor(color)}
+ key={color}
+ color={swatch}
+ size={22}
+ style={{ color: theme.white, cursor: 'pointer' }}
+ />
+ ));
+
+ return (
+
+ setOpened(false)}
+ transitionDuration={0}
+ target={
+ setOpened((o) => !o)}
+ size={22}
+ style={{ display: 'block', cursor: 'pointer' }}
+ />
+ }
+ styles={{
+ root: {
+ marginRight: theme.spacing.xs,
+ },
+ body: {
+ width: 152,
+ backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
+ },
+ arrow: {
+ backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
+ },
+ }}
+ position="bottom"
+ placement="end"
+ withArrow
+ arrowSize={3}
+ >
+ {swatches}
+
+ {type[0].toUpperCase() + type.slice(1)} color
+
+ );
+}
diff --git a/src/components/Settings/CommonSettings.tsx b/src/components/Settings/CommonSettings.tsx
index c61b6d605..ee482aa43 100644
--- a/src/components/Settings/CommonSettings.tsx
+++ b/src/components/Settings/CommonSettings.tsx
@@ -67,8 +67,9 @@ export default function CommonSettings(args: any) {
/>
)}
-
+
+
module);
return (
- {modules.map((module) => (
- {
- setConfig({
- ...config,
- modules: {
- ...config.modules,
- [module.title]: {
- ...config.modules?.[module.title],
- enabled: e.currentTarget.checked,
+ Module enabler
+
+ {modules.map((module) => (
+ {
+ setConfig({
+ ...config,
+ modules: {
+ ...config.modules,
+ [module.title]: {
+ ...config.modules?.[module.title],
+ enabled: e.currentTarget.checked,
+ },
},
- },
- });
- }}
- />
- ))}
+ });
+ }}
+ />
+ ))}
+
);
}
diff --git a/src/components/Settings/OpacitySelector.tsx b/src/components/Settings/OpacitySelector.tsx
new file mode 100644
index 000000000..f94225cd8
--- /dev/null
+++ b/src/components/Settings/OpacitySelector.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Group, Text, Slider } from '@mantine/core';
+import { useConfig } from '../../tools/state';
+
+export function OpacitySelector() {
+ const { config, setConfig } = useConfig();
+
+ const MARKS = [
+ { value: 10, label: '10' },
+ { value: 20, label: '20' },
+ { value: 30, label: '30' },
+ { value: 40, label: '40' },
+ { value: 50, label: '50' },
+ { value: 60, label: '60' },
+ { value: 70, label: '70' },
+ { value: 80, label: '80' },
+ { value: 90, label: '90' },
+ { value: 100, label: '100' },
+ ];
+
+ const setConfigOpacity = (opacity: number) => {
+ setConfig({
+ ...config,
+ settings: {
+ ...config.settings,
+ appOpacity: opacity,
+ },
+ });
+ };
+
+ return (
+
+ App Opacity
+ setConfigOpacity(value)}
+ />
+
+ );
+}
diff --git a/src/components/Settings/SettingsMenu.tsx b/src/components/Settings/SettingsMenu.tsx
index ea6e9b4a9..78b968634 100644
--- a/src/components/Settings/SettingsMenu.tsx
+++ b/src/components/Settings/SettingsMenu.tsx
@@ -11,7 +11,7 @@ function SettingsMenu(props: any) {
-
+
diff --git a/src/components/Settings/ShadeSelector.tsx b/src/components/Settings/ShadeSelector.tsx
new file mode 100644
index 000000000..ebd55e84d
--- /dev/null
+++ b/src/components/Settings/ShadeSelector.tsx
@@ -0,0 +1,97 @@
+import React, { useState } from 'react';
+import { ColorSwatch, Group, Popover, Text, useMantineTheme, MantineTheme } from '@mantine/core';
+import { useConfig } from '../../tools/state';
+import { useColorTheme } from '../../tools/color';
+
+export function ShadeSelector() {
+ const { config, setConfig } = useConfig();
+ const [opened, setOpened] = useState(false);
+
+ const { primaryColor, secondaryColor, primaryShade, setPrimaryShade } = useColorTheme();
+
+ const theme = useMantineTheme();
+ const primaryShades = theme.colors[primaryColor].map((s, i) => ({
+ swatch: theme.colors[primaryColor][i],
+ shade: i as MantineTheme['primaryShade'],
+ }));
+ const secondaryShades = theme.colors[secondaryColor].map((s, i) => ({
+ swatch: theme.colors[secondaryColor][i],
+ shade: i as MantineTheme['primaryShade'],
+ }));
+
+ const setConfigShade = (shade: MantineTheme['primaryShade']) => {
+ setPrimaryShade(shade);
+ setConfig({
+ ...config,
+ settings: {
+ ...config.settings,
+ primaryShade: shade,
+ },
+ });
+ };
+
+ const primarySwatches = primaryShades.map(({ swatch, shade }) => (
+ setConfigShade(shade)}
+ key={Number(shade)}
+ color={swatch}
+ size={22}
+ style={{ color: theme.white, cursor: 'pointer' }}
+ />
+ ));
+
+ const secondarySwatches = secondaryShades.map(({ swatch, shade }) => (
+ setConfigShade(shade)}
+ key={Number(shade)}
+ color={swatch}
+ size={22}
+ style={{ color: theme.white, cursor: 'pointer' }}
+ />
+ ));
+
+ return (
+
+ setOpened(false)}
+ transitionDuration={0}
+ target={
+ setOpened((o) => !o)}
+ size={22}
+ style={{ display: 'block', cursor: 'pointer' }}
+ />
+ }
+ styles={{
+ root: {
+ marginRight: theme.spacing.xs,
+ },
+ body: {
+ backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
+ },
+ arrow: {
+ backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
+ },
+ }}
+ position="bottom"
+ placement="end"
+ withArrow
+ arrowSize={3}
+ >
+
+ {primarySwatches}
+ {secondarySwatches}
+
+
+ Shade
+
+ );
+}
diff --git a/src/components/layout/Aside.tsx b/src/components/layout/Aside.tsx
index 8fafc5708..8ed088c4b 100644
--- a/src/components/layout/Aside.tsx
+++ b/src/components/layout/Aside.tsx
@@ -28,19 +28,22 @@ export default function Aside(props: any) {
className={cx(classes.hide)}
style={{
border: 'none',
+ background: 'none',
}}
width={{
base: 'auto',
}}
>
- {matches && (
-
-
-
-
-
-
- )}
+ <>
+ {matches && (
+
+
+
+
+
+
+ )}
+ >
);
}
diff --git a/src/components/layout/Background.tsx b/src/components/layout/Background.tsx
new file mode 100644
index 000000000..741bf9389
--- /dev/null
+++ b/src/components/layout/Background.tsx
@@ -0,0 +1,20 @@
+import { Global } from '@mantine/core';
+import { useConfig } from '../../tools/state';
+
+export function Background() {
+ const { config } = useConfig();
+
+ return (
+
+ );
+}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index 1cfb218a5..20374f609 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -41,7 +41,7 @@ export function Header(props: any) {
return (
-
+
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
index d9ccbccb7..975e51776 100644
--- a/src/components/layout/Layout.tsx
+++ b/src/components/layout/Layout.tsx
@@ -3,6 +3,7 @@ import { Header } from './Header';
import { Footer } from './Footer';
import Aside from './Aside';
import { HeaderConfig } from './HeaderConfig';
+import { Background } from './Background';
const useStyles = createStyles((theme) => ({
main: {},
@@ -13,6 +14,7 @@ export default function Layout({ children, style }: any) {
return (
} header={} footer={}>
+
@@ -26,7 +28,11 @@ export function Logo({ style }: any) {
sx={style}
weight="bold"
variant="gradient"
- gradient={{ from: 'red', to: 'orange', deg: 145 }}
+ gradient={{
+ from: primaryColor,
+ to: secondaryColor,
+ deg: 145,
+ }}
>
{config.settings.title || 'Homarr'}
diff --git a/src/components/modules/downloads/DownloadsModule.tsx b/src/components/modules/downloads/DownloadsModule.tsx
index 527a6e835..3a4a4a65b 100644
--- a/src/components/modules/downloads/DownloadsModule.tsx
+++ b/src/components/modules/downloads/DownloadsModule.tsx
@@ -55,7 +55,7 @@ export default function DownloadComponent() {
setTorrents(response.data);
setIsLoading(false);
});
- }, 1000);
+ }, 5000);
}, [config.services]);
if (downloadServices.length === 0) {
diff --git a/src/components/modules/moduleWrapper.tsx b/src/components/modules/moduleWrapper.tsx
index 2d974e53b..8d965941d 100644
--- a/src/components/modules/moduleWrapper.tsx
+++ b/src/components/modules/moduleWrapper.tsx
@@ -1,4 +1,4 @@
-import { Button, Card, Group, Menu, Switch, TextInput } from '@mantine/core';
+import { Button, Card, Group, Menu, Switch, TextInput, useMantineColorScheme } from '@mantine/core';
import { useConfig } from '../../tools/state';
import { IModule } from './modules';
@@ -91,6 +91,7 @@ function getItems(module: IModule) {
export function ModuleWrapper(props: any) {
const { module }: { module: IModule } = props;
+ const { colorScheme } = useMantineColorScheme();
const { config, setConfig } = useConfig();
const enabledModules = config.modules ?? {};
// Remove 'Module' from enabled modules titles
@@ -99,8 +100,21 @@ export function ModuleWrapper(props: any) {
if (!isShown) {
return null;
}
+
return (
-
+
(props.colorScheme);
+ const [primaryColor, setPrimaryColor] = useState('red');
+ const [secondaryColor, setSecondaryColor] = useState('orange');
+ const [primaryShade, setPrimaryShade] = useState(6);
+ const colorTheme = {
+ primaryColor,
+ secondaryColor,
+ setPrimaryColor,
+ setSecondaryColor,
+ primaryShade,
+ setPrimaryShade,
+ };
+
const toggleColorScheme = (value?: ColorScheme) => {
const nextColorScheme = value || (colorScheme === 'dark' ? 'light' : 'dark');
setColorScheme(nextColorScheme);
@@ -24,29 +37,31 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) {
return (
<>
- Homarr 🦞
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
>
);
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index c982f3940..10d347f74 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -7,6 +7,7 @@ import { Config } from '../tools/types';
import { useConfig } from '../tools/state';
import { migrateToIdConfig } from '../tools/migrate';
import { getConfig } from '../tools/getConfig';
+import { useColorTheme } from '../tools/color';
import Layout from '../components/layout/Layout';
export async function getServerSideProps({
@@ -29,8 +30,11 @@ export async function getServerSideProps({
export default function HomePage(props: any) {
const { config: initialConfig }: { config: Config } = props;
const { setConfig } = useConfig();
+ const { setPrimaryColor, setSecondaryColor } = useColorTheme();
useEffect(() => {
const migratedConfig = migrateToIdConfig(initialConfig);
+ setPrimaryColor(migratedConfig.settings.primaryColor || 'red');
+ setSecondaryColor(migratedConfig.settings.secondaryColor || 'orange');
setConfig(migratedConfig);
}, [initialConfig]);
return (
diff --git a/src/tools/color.ts b/src/tools/color.ts
new file mode 100644
index 000000000..6e92bfab9
--- /dev/null
+++ b/src/tools/color.ts
@@ -0,0 +1,28 @@
+import { createContext, useContext } from 'react';
+import { MantineTheme } from '@mantine/core';
+
+type colorThemeContextType = {
+ primaryColor: MantineTheme['primaryColor'];
+ secondaryColor: MantineTheme['primaryColor'];
+ primaryShade: MantineTheme['primaryShade'];
+ setPrimaryColor: (color: MantineTheme['primaryColor']) => void;
+ setSecondaryColor: (color: MantineTheme['primaryColor']) => void;
+ setPrimaryShade: (shade: MantineTheme['primaryShade']) => void;
+};
+
+export const ColorTheme = createContext({
+ primaryColor: 'red',
+ secondaryColor: 'orange',
+ primaryShade: 6,
+ setPrimaryColor: () => {},
+ setSecondaryColor: () => {},
+ setPrimaryShade: () => {},
+});
+
+export function useColorTheme() {
+ const context = useContext(ColorTheme);
+ if (context === undefined) {
+ throw new Error('useColorTheme must be used within a ColorTheme.Provider');
+ }
+ return context;
+}
diff --git a/src/tools/theme.ts b/src/tools/theme.ts
index 32b492886..69d7b9643 100644
--- a/src/tools/theme.ts
+++ b/src/tools/theme.ts
@@ -1,6 +1,3 @@
import { MantineProviderProps } from '@mantine/core';
-export const theme: MantineProviderProps['theme'] = {
- primaryColor: 'red',
- primaryShade: 6,
-};
+export const theme: MantineProviderProps['theme'] = {};
diff --git a/src/tools/types.ts b/src/tools/types.ts
index e817fd459..365ef24ab 100644
--- a/src/tools/types.ts
+++ b/src/tools/types.ts
@@ -1,3 +1,4 @@
+import { MantineTheme } from '@mantine/core';
import { OptionValues } from '../components/modules/modules';
export interface Settings {
@@ -5,6 +6,11 @@ export interface Settings {
title?: string;
logo?: string;
favicon?: string;
+ primaryColor?: MantineTheme['primaryColor'];
+ secondaryColor?: MantineTheme['primaryColor'];
+ primaryShade?: MantineTheme['primaryShade'];
+ background?: string;
+ appOpacity?: number;
}
export interface Config {