mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-13 17:05:47 +01:00
✨ Image properties customization (#1590)
This commit is contained in:
@@ -18,6 +18,29 @@
|
|||||||
"background": {
|
"background": {
|
||||||
"label": "Background"
|
"label": "Background"
|
||||||
},
|
},
|
||||||
|
"backgroundImageAttachment": {
|
||||||
|
"label": "Background image attachment",
|
||||||
|
"options": {
|
||||||
|
"fixed": "Fixed - Background stays in the same position (recommended)",
|
||||||
|
"scroll": "Scroll - Background scrolls with your mouse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backgroundImageSize": {
|
||||||
|
"label": "Background image size",
|
||||||
|
"options": {
|
||||||
|
"cover": "Cover - Scales the image as small as possible to cover the entire window by cropping excessive space. (recommended)",
|
||||||
|
"contain": "Contain - Scales the image as large as possible within its container without cropping or stretching the image."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backgroundImageRepeat": {
|
||||||
|
"label": "Background image attachment",
|
||||||
|
"options": {
|
||||||
|
"repeat": "Repeat - The image is repeated as much as needed to cover the whole background image painting area.",
|
||||||
|
"no-repeat": "No repeat - The image is not repeated any may not fill the entire space (recommended)",
|
||||||
|
"repeat-x": "Repeat X - Same as 'Repeat' but only on horizontal axis.",
|
||||||
|
"repeat-y": "Repeat Y - Same as 'Repeat' but only on vertical axis."
|
||||||
|
}
|
||||||
|
},
|
||||||
"customCSS": {
|
"customCSS": {
|
||||||
"label": "Custom CSS",
|
"label": "Custom CSS",
|
||||||
"description": "Further, customize your dashboard using CSS, only recommended for experienced users",
|
"description": "Further, customize your dashboard using CSS, only recommended for experienced users",
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Input,
|
Input,
|
||||||
MantineTheme,
|
MantineTheme,
|
||||||
|
Select,
|
||||||
Slider,
|
Slider,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
createStyles,
|
createStyles,
|
||||||
rem,
|
rem,
|
||||||
@@ -16,6 +16,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { highlight, languages } from 'prismjs';
|
import { highlight, languages } from 'prismjs';
|
||||||
import Editor from 'react-simple-code-editor';
|
import Editor from 'react-simple-code-editor';
|
||||||
import { useColorTheme } from '~/tools/color';
|
import { useColorTheme } from '~/tools/color';
|
||||||
|
import { BackgroundImageAttachment, BackgroundImageRepeat, BackgroundImageSize } from '~/types/settings';
|
||||||
|
|
||||||
import { useBoardCustomizationFormContext } from '../form';
|
import { useBoardCustomizationFormContext } from '../form';
|
||||||
|
|
||||||
@@ -30,6 +31,32 @@ export const AppearanceCustomization = () => {
|
|||||||
placeholder="/imgs/backgrounds/background.png"
|
placeholder="/imgs/backgrounds/background.png"
|
||||||
{...form.getInputProps('appearance.backgroundSrc')}
|
{...form.getInputProps('appearance.backgroundSrc')}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
label={t('backgroundImageAttachment.label')}
|
||||||
|
data={BackgroundImageAttachment.map((attachment) => ({
|
||||||
|
value: attachment,
|
||||||
|
label: t(`backgroundImageAttachment.options.${attachment}`) as string,
|
||||||
|
}))}
|
||||||
|
{...form.getInputProps('appearance.backgroundImageAttachment')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={t('backgroundImageSize.label')}
|
||||||
|
data={BackgroundImageSize.map((size) => ({
|
||||||
|
value: size,
|
||||||
|
label: t(`backgroundImageSize.options.${size}`) as string,
|
||||||
|
}))}
|
||||||
|
{...form.getInputProps('appearance.backgroundImageSize')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={t('backgroundImageRepeat.label')}
|
||||||
|
data={BackgroundImageRepeat.map((repeat) => ({
|
||||||
|
value: repeat,
|
||||||
|
label: t(`backgroundImageRepeat.options.${repeat}`) as string,
|
||||||
|
}))}
|
||||||
|
{...form.getInputProps('appearance.backgroundImageRepeat')}
|
||||||
|
/>
|
||||||
<ColorSelector type="primaryColor" />
|
<ColorSelector type="primaryColor" />
|
||||||
<ColorSelector type="secondaryColor" />
|
<ColorSelector type="secondaryColor" />
|
||||||
<ShadeSelector />
|
<ShadeSelector />
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import { openContextModal } from '@mantine/modals';
|
|||||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||||
import {
|
import {
|
||||||
IconApps,
|
IconApps,
|
||||||
IconBrandDocker,
|
|
||||||
IconEditCircle,
|
IconEditCircle,
|
||||||
IconEditCircleOff,
|
IconEditCircleOff,
|
||||||
IconSettings,
|
IconSettings
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import Consola from 'consola';
|
import Consola from 'consola';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
@@ -19,11 +18,10 @@ import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/grid
|
|||||||
import { BoardHeadOverride } from '~/components/layout/Meta/BoardHeadOverride';
|
import { BoardHeadOverride } from '~/components/layout/Meta/BoardHeadOverride';
|
||||||
import { HeaderActionButton } from '~/components/layout/header/ActionButton';
|
import { HeaderActionButton } from '~/components/layout/header/ActionButton';
|
||||||
import { useConfigContext } from '~/config/provider';
|
import { useConfigContext } from '~/config/provider';
|
||||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
import { MainLayout } from './MainLayout';
|
|
||||||
import { env } from 'process';
|
import { env } from 'process';
|
||||||
|
import { MainLayout } from './MainLayout';
|
||||||
|
|
||||||
type BoardLayoutProps = {
|
type BoardLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -205,8 +203,9 @@ const BackgroundImage = () => {
|
|||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`,
|
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`,
|
||||||
backgroundPosition: 'center center',
|
backgroundPosition: 'center center',
|
||||||
backgroundSize: 'cover',
|
backgroundSize: config?.settings.customization.backgroundImageSize ?? 'cover',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: config?.settings.customization.backgroundImageRepeat ?? 'no-repeat',
|
||||||
|
backgroundAttachment: config?.settings.customization.backgroundImageAttachment ?? 'fixed'
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function CustomizationPage({
|
|||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { mutateAsync: saveCusomization, isLoading } = api.config.saveCusomization.useMutation();
|
const { mutateAsync: saveCustomization, isLoading } = api.config.saveCustomization.useMutation();
|
||||||
const { i18nZodResolver } = useI18nZodResolver();
|
const { i18nZodResolver } = useI18nZodResolver();
|
||||||
const { t } = useTranslation('boards/customize');
|
const { t } = useTranslation('boards/customize');
|
||||||
const form = useBoardCustomizationForm({
|
const form = useBoardCustomizationForm({
|
||||||
@@ -86,6 +86,9 @@ export default function CustomizationPage({
|
|||||||
shade: (config?.settings.customization.colors.shade as number | undefined) ?? 8,
|
shade: (config?.settings.customization.colors.shade as number | undefined) ?? 8,
|
||||||
opacity: config?.settings.customization.appOpacity ?? 50,
|
opacity: config?.settings.customization.appOpacity ?? 50,
|
||||||
customCss: config?.settings.customization.customCss ?? '',
|
customCss: config?.settings.customization.customCss ?? '',
|
||||||
|
backgroundImageAttachment: config?.settings.customization.backgroundImageAttachment ?? 'fixed',
|
||||||
|
backgroundImageRepeat: config?.settings.customization.backgroundImageRepeat ?? 'no-repeat',
|
||||||
|
backgroundImageSize: config?.settings.customization.backgroundImageSize ?? 'cover',
|
||||||
},
|
},
|
||||||
gridstack: {
|
gridstack: {
|
||||||
sm: config?.settings.customization.gridstack?.columnCountSmall ?? 3,
|
sm: config?.settings.customization.gridstack?.columnCountSmall ?? 3,
|
||||||
@@ -114,7 +117,7 @@ export default function CustomizationPage({
|
|||||||
message: t('notifications.pending.message'),
|
message: t('notifications.pending.message'),
|
||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
await saveCusomization(
|
await saveCustomization(
|
||||||
{
|
{
|
||||||
name: query.slug,
|
name: query.slug,
|
||||||
...values,
|
...values,
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export const configRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return await getFrontendConfig(input.name);
|
return await getFrontendConfig(input.name);
|
||||||
}),
|
}),
|
||||||
saveCusomization: adminProcedure
|
saveCustomization: adminProcedure
|
||||||
.input(boardCustomizationSchema.and(z.object({ name: configNameSchema })))
|
.input(boardCustomizationSchema.and(z.object({ name: configNameSchema })))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const previousConfig = getConfig(input.name);
|
const previousConfig = getConfig(input.name);
|
||||||
@@ -193,6 +193,9 @@ export const configRouter = createTRPCRouter({
|
|||||||
...previousConfig.settings.customization,
|
...previousConfig.settings.customization,
|
||||||
appOpacity: input.appearance.opacity,
|
appOpacity: input.appearance.opacity,
|
||||||
backgroundImageUrl: input.appearance.backgroundSrc,
|
backgroundImageUrl: input.appearance.backgroundSrc,
|
||||||
|
backgroundImageAttachment: input.appearance.backgroundImageAttachment,
|
||||||
|
backgroundImageRepeat: input.appearance.backgroundImageRepeat,
|
||||||
|
backgroundImageSize: input.appearance.backgroundImageSize,
|
||||||
colors: {
|
colors: {
|
||||||
primary: input.appearance.primaryColor,
|
primary: input.appearance.primaryColor,
|
||||||
secondary: input.appearance.secondaryColor,
|
secondary: input.appearance.secondaryColor,
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ export interface CustomizationSettingsType {
|
|||||||
logoImageUrl?: string;
|
logoImageUrl?: string;
|
||||||
faviconUrl?: string;
|
faviconUrl?: string;
|
||||||
backgroundImageUrl?: string;
|
backgroundImageUrl?: string;
|
||||||
|
backgroundImageAttachment?: typeof BackgroundImageAttachment[number];
|
||||||
|
backgroundImageSize?: typeof BackgroundImageSize[number];
|
||||||
|
backgroundImageRepeat?: typeof BackgroundImageRepeat[number];
|
||||||
customCss?: string;
|
customCss?: string;
|
||||||
colors: ColorsCustomizationSettingsType;
|
colors: ColorsCustomizationSettingsType;
|
||||||
appOpacity?: number;
|
appOpacity?: number;
|
||||||
@@ -52,6 +55,12 @@ export interface CustomizationSettingsType {
|
|||||||
accessibility: AccessibilitySettings;
|
accessibility: AccessibilitySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BackgroundImageAttachment = ['fixed', 'scroll'] as const;
|
||||||
|
|
||||||
|
export const BackgroundImageSize = ['cover', 'contain'] as const;
|
||||||
|
|
||||||
|
export const BackgroundImageRepeat = ['no-repeat', 'repeat', 'repeat-x', 'repeat-y'] as const;
|
||||||
|
|
||||||
export interface AccessibilitySettings {
|
export interface AccessibilitySettings {
|
||||||
disablePingPulse: boolean;
|
disablePingPulse: boolean;
|
||||||
replacePingDotsWithIcons: boolean;
|
replacePingDotsWithIcons: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { BackgroundImageAttachment, BackgroundImageRepeat, BackgroundImageSize } from '~/types/settings';
|
||||||
|
|
||||||
export const createBoardSchemaValidation = z.object({
|
export const createBoardSchemaValidation = z.object({
|
||||||
name: z.string().min(2).max(25),
|
name: z.string().min(2).max(25),
|
||||||
@@ -27,6 +28,9 @@ export const boardCustomizationSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
appearance: z.object({
|
appearance: z.object({
|
||||||
backgroundSrc: z.string(),
|
backgroundSrc: z.string(),
|
||||||
|
backgroundImageAttachment: z.enum(BackgroundImageAttachment),
|
||||||
|
backgroundImageSize: z.enum(BackgroundImageSize),
|
||||||
|
backgroundImageRepeat: z.enum(BackgroundImageRepeat),
|
||||||
primaryColor: z.custom<MantineColor>(
|
primaryColor: z.custom<MantineColor>(
|
||||||
(value) => typeof value === 'string' && MANTINE_COLORS.includes(value)
|
(value) => typeof value === 'string' && MANTINE_COLORS.includes(value)
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user