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": {
|
||||
"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": {
|
||||
"label": "Custom CSS",
|
||||
"description": "Further, customize your dashboard using CSS, only recommended for experienced users",
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
Group,
|
||||
Input,
|
||||
MantineTheme,
|
||||
Select,
|
||||
Slider,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
createStyles,
|
||||
rem,
|
||||
@@ -16,6 +16,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import { highlight, languages } from 'prismjs';
|
||||
import Editor from 'react-simple-code-editor';
|
||||
import { useColorTheme } from '~/tools/color';
|
||||
import { BackgroundImageAttachment, BackgroundImageRepeat, BackgroundImageSize } from '~/types/settings';
|
||||
|
||||
import { useBoardCustomizationFormContext } from '../form';
|
||||
|
||||
@@ -30,6 +31,32 @@ export const AppearanceCustomization = () => {
|
||||
placeholder="/imgs/backgrounds/background.png"
|
||||
{...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="secondaryColor" />
|
||||
<ShadeSelector />
|
||||
|
||||
@@ -4,10 +4,9 @@ import { openContextModal } from '@mantine/modals';
|
||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconApps,
|
||||
IconBrandDocker,
|
||||
IconEditCircle,
|
||||
IconEditCircleOff,
|
||||
IconSettings,
|
||||
IconSettings
|
||||
} from '@tabler/icons-react';
|
||||
import Consola from 'consola';
|
||||
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 { HeaderActionButton } from '~/components/layout/header/ActionButton';
|
||||
import { useConfigContext } from '~/config/provider';
|
||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
import { MainLayout } from './MainLayout';
|
||||
import { env } from 'process';
|
||||
import { MainLayout } from './MainLayout';
|
||||
|
||||
type BoardLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -205,8 +203,9 @@ const BackgroundImage = () => {
|
||||
minHeight: '100vh',
|
||||
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`,
|
||||
backgroundPosition: 'center center',
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: config?.settings.customization.backgroundImageSize ?? 'cover',
|
||||
backgroundRepeat: config?.settings.customization.backgroundImageRepeat ?? 'no-repeat',
|
||||
backgroundAttachment: config?.settings.customization.backgroundImageAttachment ?? 'fixed'
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function CustomizationPage({
|
||||
refetchOnMount: false,
|
||||
}
|
||||
);
|
||||
const { mutateAsync: saveCusomization, isLoading } = api.config.saveCusomization.useMutation();
|
||||
const { mutateAsync: saveCustomization, isLoading } = api.config.saveCustomization.useMutation();
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
const { t } = useTranslation('boards/customize');
|
||||
const form = useBoardCustomizationForm({
|
||||
@@ -86,6 +86,9 @@ export default function CustomizationPage({
|
||||
shade: (config?.settings.customization.colors.shade as number | undefined) ?? 8,
|
||||
opacity: config?.settings.customization.appOpacity ?? 50,
|
||||
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: {
|
||||
sm: config?.settings.customization.gridstack?.columnCountSmall ?? 3,
|
||||
@@ -114,7 +117,7 @@ export default function CustomizationPage({
|
||||
message: t('notifications.pending.message'),
|
||||
loading: true,
|
||||
});
|
||||
await saveCusomization(
|
||||
await saveCustomization(
|
||||
{
|
||||
name: query.slug,
|
||||
...values,
|
||||
|
||||
@@ -177,7 +177,7 @@ export const configRouter = createTRPCRouter({
|
||||
|
||||
return await getFrontendConfig(input.name);
|
||||
}),
|
||||
saveCusomization: adminProcedure
|
||||
saveCustomization: adminProcedure
|
||||
.input(boardCustomizationSchema.and(z.object({ name: configNameSchema })))
|
||||
.mutation(async ({ input }) => {
|
||||
const previousConfig = getConfig(input.name);
|
||||
@@ -193,6 +193,9 @@ export const configRouter = createTRPCRouter({
|
||||
...previousConfig.settings.customization,
|
||||
appOpacity: input.appearance.opacity,
|
||||
backgroundImageUrl: input.appearance.backgroundSrc,
|
||||
backgroundImageAttachment: input.appearance.backgroundImageAttachment,
|
||||
backgroundImageRepeat: input.appearance.backgroundImageRepeat,
|
||||
backgroundImageSize: input.appearance.backgroundImageSize,
|
||||
colors: {
|
||||
primary: input.appearance.primaryColor,
|
||||
secondary: input.appearance.secondaryColor,
|
||||
|
||||
@@ -45,6 +45,9 @@ export interface CustomizationSettingsType {
|
||||
logoImageUrl?: string;
|
||||
faviconUrl?: string;
|
||||
backgroundImageUrl?: string;
|
||||
backgroundImageAttachment?: typeof BackgroundImageAttachment[number];
|
||||
backgroundImageSize?: typeof BackgroundImageSize[number];
|
||||
backgroundImageRepeat?: typeof BackgroundImageRepeat[number];
|
||||
customCss?: string;
|
||||
colors: ColorsCustomizationSettingsType;
|
||||
appOpacity?: number;
|
||||
@@ -52,6 +55,12 @@ export interface CustomizationSettingsType {
|
||||
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 {
|
||||
disablePingPulse: boolean;
|
||||
replacePingDotsWithIcons: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
||||
import { z } from 'zod';
|
||||
import { BackgroundImageAttachment, BackgroundImageRepeat, BackgroundImageSize } from '~/types/settings';
|
||||
|
||||
export const createBoardSchemaValidation = z.object({
|
||||
name: z.string().min(2).max(25),
|
||||
@@ -27,6 +28,9 @@ export const boardCustomizationSchema = z.object({
|
||||
}),
|
||||
appearance: z.object({
|
||||
backgroundSrc: z.string(),
|
||||
backgroundImageAttachment: z.enum(BackgroundImageAttachment),
|
||||
backgroundImageSize: z.enum(BackgroundImageSize),
|
||||
backgroundImageRepeat: z.enum(BackgroundImageRepeat),
|
||||
primaryColor: z.custom<MantineColor>(
|
||||
(value) => typeof value === 'string' && MANTINE_COLORS.includes(value)
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user