mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 23:15:46 +01:00
@@ -1,14 +1,19 @@
|
||||
const { i18n } = require('./next-i18next.config');
|
||||
|
||||
const removeImports = require('next-remove-imports')();
|
||||
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
images: {
|
||||
domains: ['cdn.jsdelivr.net'],
|
||||
},
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
i18n,
|
||||
});
|
||||
module.exports = withBundleAnalyzer(
|
||||
removeImports({
|
||||
experimental: { esmExternals: true },
|
||||
images: {
|
||||
domains: ['cdn.jsdelivr.net'],
|
||||
},
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
i18n,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"@tabler/icons": "^1.106.0",
|
||||
"@tanstack/react-query": "^4.2.1",
|
||||
"@tanstack/react-query-devtools": "^4.24.4",
|
||||
"@uiw/react-textarea-code-editor": "v1.4.4",
|
||||
"axios": "^0.27.2",
|
||||
"consola": "^2.15.3",
|
||||
"cookies-next": "^2.1.1",
|
||||
@@ -59,6 +60,7 @@
|
||||
"js-file-download": "^0.4.12",
|
||||
"next": "^13.1.6",
|
||||
"next-i18next": "^11.3.0",
|
||||
"next-remove-imports": "^1.0.8",
|
||||
"nzbget-api": "^0.0.3",
|
||||
"ping": "^0.4.2",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
@@ -81,6 +83,8 @@
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
||||
"@typescript-eslint/parser": "^5.30.7",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-transform-remove-imports": "^1.7.0",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
|
||||
@@ -24,5 +24,11 @@
|
||||
"seconds": "seconds",
|
||||
"minutes": "minutes",
|
||||
"hours": "hours"
|
||||
},
|
||||
"loading": "Loading...",
|
||||
"breakPoints": {
|
||||
"small": "small",
|
||||
"medium": "medium",
|
||||
"large": "large"
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,5 @@
|
||||
"popover": {
|
||||
"title": "Edit mode is enabled for <1>{{size}}</1> size",
|
||||
"text": "You can adjust and configure your apps now. Changes are <strong>not saved</strong> until you exit edit mode"
|
||||
},
|
||||
"screenSizes": {
|
||||
"small": "small",
|
||||
"medium": "medium",
|
||||
"large": "large"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"description": "Homarr is a <strong>sleek</strong>, <strong>modern</strong> dashboard that puts all of your apps and services at your fingertips. With Homarr, you can access and control everything in one convenient location. Homarr seamlessly integrates with the apps you've added, providing you with valuable information and giving you complete control. Installation is a breeze, and Homarr supports a wide range of deployment methods.",
|
||||
"i18n": "Loaded I18n translation namespaces",
|
||||
"locales": "Configured I18n locales",
|
||||
"contact": "Having trouble or questions? Connect with us!",
|
||||
"addToDashboard": "Add to Dashboard"
|
||||
}
|
||||
"addToDashboard": "Add to Dashboard",
|
||||
"metrics": {
|
||||
"configurationSchemaVersion": "Configuration schema version",
|
||||
"configurationsCount": "Available configurations",
|
||||
"version": "Version",
|
||||
"nodeEnvironment": "Node environment",
|
||||
"i18n": "Loaded I18n translation namespaces",
|
||||
"locales": "Configured I18n locales"
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,11 @@
|
||||
},
|
||||
"grow": "Grow grid (take all space)",
|
||||
"layout": {
|
||||
"title": "Dashboard layout",
|
||||
"preview": {
|
||||
"title": "Preview",
|
||||
"subtitle": "Changes will be saved automatically"
|
||||
},
|
||||
"divider": "Layout options",
|
||||
"main": "Main",
|
||||
"sidebar": "Sidebar",
|
||||
"cannotturnoff": "Cannot be turned off",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"colors": "Colors",
|
||||
"suffix": "{{color}} color"
|
||||
}
|
||||
21
public/locales/en/settings/customization/general.json
Normal file
21
public/locales/en/settings/customization/general.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"text": "Customizations allow you to configure and adjust your experience with Homarr to your preferences.",
|
||||
"accordeon": {
|
||||
"layout": {
|
||||
"name": "Layout",
|
||||
"description": "Enable and disable elements on your header and dashboard tiles"
|
||||
},
|
||||
"gridstack": {
|
||||
"name": "Gridstack",
|
||||
"description": "Customize the behaviour and columns of your dashboard area"
|
||||
},
|
||||
"pageMetadata": {
|
||||
"name": "Page Metadata",
|
||||
"description": "Adjust titles, logo and PWA"
|
||||
},
|
||||
"appereance": {
|
||||
"name": "Appereance",
|
||||
"description": "Customize the background, colors and apps appereance"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
public/locales/en/settings/customization/gridstack.json
Normal file
10
public/locales/en/settings/customization/gridstack.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"columnsCount": {
|
||||
"labelPreset": "Columns in {{size}} size",
|
||||
"descriptionPreset": "Number of columns when the screen is less than {{pixels}} pixels wide",
|
||||
"descriptionExceedsPreset": "Number of columns when the screen size exceeds {{pixels}} pixels"
|
||||
},
|
||||
"unsavedChanges": "You have unsaved changes. Click the Apply changes button below to apply and save.",
|
||||
"applyChanges": "Apply changes",
|
||||
"defaultValues": "Default values"
|
||||
}
|
||||
@@ -1,22 +1,28 @@
|
||||
{
|
||||
"pageTitle": {
|
||||
"label": "Page Title"
|
||||
"label": "Page Title",
|
||||
"description": "The dashboard title at the top left"
|
||||
},
|
||||
"metaTitle": {
|
||||
"label": "Meta Title"
|
||||
"label": "Meta Title",
|
||||
"description": "The title, that is being displayed as your tab name"
|
||||
},
|
||||
"logo": {
|
||||
"label": "Logo"
|
||||
"label": "Logo",
|
||||
"description": "The dashboard logo at the top left"
|
||||
},
|
||||
"favicon": {
|
||||
"label": "Favicon"
|
||||
"label": "Favicon",
|
||||
"description": "The icon, that is being used in front of your tab name"
|
||||
},
|
||||
"background": {
|
||||
"label": "Background"
|
||||
},
|
||||
"customCSS": {
|
||||
"label": "Custom CSS",
|
||||
"placeholder": "Custom CSS will be applied last"
|
||||
"description": "Customize all elements on your dashboard, only recommended for experienced users",
|
||||
"placeholder": "Custom CSS will be applied last",
|
||||
"applying": "Applying CSS..."
|
||||
},
|
||||
"buttons": {
|
||||
"submit": "Submit"
|
||||
|
||||
@@ -82,7 +82,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
<ActionIcon className={classes.informationIcon} variant="default">
|
||||
{item.icon}
|
||||
</ActionIcon>
|
||||
{t(item.label)}
|
||||
{t(`layout/modals/about:metrics.${item.label}`)}
|
||||
</Group>
|
||||
</td>
|
||||
<td className={classes.informationTableColumn}>{item.content}</td>
|
||||
@@ -151,7 +151,6 @@ interface ExtendedInitOptions extends InitOptions {
|
||||
}
|
||||
|
||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
||||
// TODO: Fix this to not request. Pass it as a prop.
|
||||
const colorGradiant = usePrimaryGradient();
|
||||
const { attributes } = usePackageAttributesStore();
|
||||
|
||||
@@ -168,7 +167,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
...items,
|
||||
{
|
||||
icon: <IconLanguage size={20} />,
|
||||
label: 'layout/modals/about:i18n',
|
||||
label: 'i18n',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
{usedI18nNamespaces.length}
|
||||
@@ -177,7 +176,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
},
|
||||
{
|
||||
icon: <IconVocabulary size={20} />,
|
||||
label: 'layout/modals/about:locales',
|
||||
label: 'locales',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
{initOptions.locales.length}
|
||||
@@ -190,7 +189,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
items = [
|
||||
{
|
||||
icon: <IconSchema size={20} />,
|
||||
label: 'Configuration schema version',
|
||||
label: 'configurationSchemaVersion',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
{configVersion}
|
||||
@@ -199,7 +198,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
},
|
||||
{
|
||||
icon: <IconFile size={20} />,
|
||||
label: 'Available configurations',
|
||||
label: 'configurationsCount',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
{configs.length}
|
||||
@@ -249,7 +248,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
},
|
||||
{
|
||||
icon: <IconAnchor size={20} />,
|
||||
label: 'Node environment',
|
||||
label: 'nodeEnvironment',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
{attributes.environment}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useMantineTheme } from '@mantine/core';
|
||||
import create from 'zustand';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { GridstackBreakpoints } from '../../../../constants/gridstack-breakpoints';
|
||||
|
||||
export const useGridstackStore = create<GridstackStoreType>((set, get) => ({
|
||||
mainAreaWidth: null,
|
||||
@@ -27,18 +29,28 @@ export const useNamedWrapperColumnCount = (): 'small' | 'medium' | 'large' | nul
|
||||
};
|
||||
|
||||
export const useWrapperColumnCount = () => {
|
||||
const { config } = useConfigContext();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (useNamedWrapperColumnCount()) {
|
||||
case 'large':
|
||||
return 12;
|
||||
return config.settings.customization.gridstack?.columnCountLarge ?? 12;
|
||||
case 'medium':
|
||||
return 6;
|
||||
return config.settings.customization.gridstack?.columnCountMedium ?? 6;
|
||||
case 'small':
|
||||
return 3;
|
||||
return config.settings.customization.gridstack?.columnCountSmall ?? 3;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function getCurrentShapeSize(size: number) {
|
||||
return size >= 1400 ? 'lg' : size >= 768 ? 'md' : 'sm';
|
||||
return size >= GridstackBreakpoints.large
|
||||
? 'lg'
|
||||
: size >= GridstackBreakpoints.medium
|
||||
? 'md'
|
||||
: 'sm';
|
||||
}
|
||||
|
||||
117
src/components/Settings/Customization/CustomizationAccordeon.tsx
Normal file
117
src/components/Settings/Customization/CustomizationAccordeon.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Accordion, Grid, Group, Stack, Text } from '@mantine/core';
|
||||
import { IconBrush, IconChartCandle, IconDragDrop, IconLayout } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ReactNode } from 'react';
|
||||
import { GridstackConfiguration } from './Layout/GridstackConfiguration';
|
||||
import { LayoutSelector } from './Layout/LayoutSelector';
|
||||
import { BackgroundChanger } from './Meta/BackgroundChanger';
|
||||
import { FaviconChanger } from './Meta/FaviconChanger';
|
||||
import { LogoImageChanger } from './Meta/LogoImageChanger';
|
||||
import { BrowserTabTitle } from './Meta/MetaTitleChanger';
|
||||
import { DashboardTitleChanger } from './Meta/PageTitleChanger';
|
||||
import { ColorSelector } from './Theme/ColorSelector';
|
||||
import { CustomCssChanger } from './Theme/CustomCssChanger';
|
||||
import { DashboardTilesOpacitySelector } from './Theme/OpacitySelector';
|
||||
import { ShadeSelector } from './Theme/ShadeSelector';
|
||||
|
||||
export const CustomizationSettingsAccordeon = () => {
|
||||
const items = getItems().map((item) => (
|
||||
<Accordion.Item value={item.id} key={item.label}>
|
||||
<Accordion.Control>
|
||||
<AccordionLabel {...item} />
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Text size="sm">{item.content}</Text>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
));
|
||||
return (
|
||||
<Accordion variant="contained" chevronPosition="right">
|
||||
{items}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
interface AccordionLabelProps {
|
||||
label: string;
|
||||
image: ReactNode;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const AccordionLabel = ({ label, image, description }: AccordionLabelProps) => (
|
||||
<Group noWrap>
|
||||
{image}
|
||||
<div>
|
||||
<Text>{label}</Text>
|
||||
<Text size="sm" color="dimmed" weight={400}>
|
||||
{description}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
|
||||
const getItems = () => {
|
||||
const { t } = useTranslation([
|
||||
'settings/customization/general',
|
||||
'settings/customization/color-selector',
|
||||
]);
|
||||
return [
|
||||
{
|
||||
id: 'layout',
|
||||
image: <IconLayout />,
|
||||
label: t('accordeon.layout.name'),
|
||||
description: t('accordeon.layout.description'),
|
||||
content: <LayoutSelector />,
|
||||
},
|
||||
{
|
||||
id: 'gridstack',
|
||||
image: <IconDragDrop />,
|
||||
label: t('accordeon.gridstack.name'),
|
||||
description: t('accordeon.gridstack.description'),
|
||||
content: <GridstackConfiguration />,
|
||||
},
|
||||
{
|
||||
id: 'page_metadata',
|
||||
image: <IconChartCandle />,
|
||||
label: t('accordeon.pageMetadata.name'),
|
||||
description: t('accordeon.pageMetadata.description'),
|
||||
content: (
|
||||
<>
|
||||
<DashboardTitleChanger />
|
||||
<BrowserTabTitle />
|
||||
<LogoImageChanger />
|
||||
<FaviconChanger />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'appereance',
|
||||
image: <IconBrush />,
|
||||
label: t('accordeon.appereance.name'),
|
||||
description: t('accordeon.appereance.description'),
|
||||
content: (
|
||||
<>
|
||||
<BackgroundChanger />
|
||||
|
||||
<Stack spacing="xs" my="md">
|
||||
<Text>{t('settings/customization/color-selector:colors')}</Text>
|
||||
<Grid>
|
||||
<Grid.Col sm={12} md={6}>
|
||||
<ColorSelector type="primary" defaultValue="red" />
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={12} md={6}>
|
||||
<ColorSelector type="secondary" defaultValue="orange" />
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={12} md={6}>
|
||||
<ShadeSelector />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
|
||||
<DashboardTilesOpacitySelector />
|
||||
<CustomCssChanger />
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,46 +1,19 @@
|
||||
import { ScrollArea, Stack } from '@mantine/core';
|
||||
import { ScrollArea, Stack, Text } from '@mantine/core';
|
||||
import { useViewportSize } from '@mantine/hooks';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { LayoutSelector } from './Layout/LayoutSelector';
|
||||
import { BackgroundChanger } from './Meta/BackgroundChanger';
|
||||
import { FaviconChanger } from './Meta/FaviconChanger';
|
||||
import { LogoImageChanger } from './Meta/LogoImageChanger';
|
||||
import { MetaTitleChanger } from './Meta/MetaTitleChanger';
|
||||
import { PageTitleChanger } from './Meta/PageTitleChanger';
|
||||
import { ColorSelector } from './Theme/ColorSelector';
|
||||
import { CustomCssChanger } from './Theme/CustomCssChanger';
|
||||
import { OpacitySelector } from './Theme/OpacitySelector';
|
||||
import { ShadeSelector } from './Theme/ShadeSelector';
|
||||
import { CustomizationSettingsAccordeon } from './CustomizationAccordeon';
|
||||
|
||||
export default function CustomizationSettings() {
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const { t } = useTranslation('common');
|
||||
const { height, width } = useViewportSize();
|
||||
|
||||
const { updateConfig } = useConfigStore();
|
||||
const { height } = useViewportSize();
|
||||
const { t } = useTranslation('settings/customization/general');
|
||||
|
||||
return (
|
||||
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
||||
<Stack mt="xs" mb="md" spacing="xs">
|
||||
<LayoutSelector defaultLayout={config?.settings.customization.layout} />
|
||||
<PageTitleChanger defaultValue={config?.settings.customization.pageTitle} />
|
||||
<MetaTitleChanger defaultValue={config?.settings.customization.metaTitle} />
|
||||
<LogoImageChanger defaultValue={config?.settings.customization.logoImageUrl} />
|
||||
<FaviconChanger defaultValue={config?.settings.customization.faviconUrl} />
|
||||
<BackgroundChanger defaultValue={config?.settings.customization.backgroundImageUrl} />
|
||||
<CustomCssChanger defaultValue={config?.settings.customization.customCss} />
|
||||
<ColorSelector
|
||||
type="primary"
|
||||
defaultValue={config?.settings.customization.colors.primary}
|
||||
/>
|
||||
<ColorSelector
|
||||
type="secondary"
|
||||
defaultValue={config?.settings.customization.colors.secondary}
|
||||
/>
|
||||
<ShadeSelector defaultValue={config?.settings.customization.colors.shade} />
|
||||
<OpacitySelector defaultValue={config?.settings.customization.appOpacity} />
|
||||
<Text color="dimmed">
|
||||
{t('text')}
|
||||
</Text>
|
||||
<CustomizationSettingsAccordeon />
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Alert, Button, Grid, Input, LoadingOverlay, Slider } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconCheck, IconReload } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { GridstackBreakpoints } from '../../../../constants/gridstack-breakpoints';
|
||||
import { sleep } from '../../../../tools/client/time';
|
||||
import { GridstackSettingsType } from '../../../../types/settings';
|
||||
|
||||
export const GridstackConfiguration = () => {
|
||||
const { t } = useTranslation(['settings/customization/gridstack', 'common']);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
if (!config || !configName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValue = config.settings.customization?.gridstack ?? {
|
||||
columnCountSmall: 3,
|
||||
columnCountMedium: 6,
|
||||
columnCountLarge: 12,
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValue,
|
||||
});
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSubmit = async (values: GridstackSettingsType) => {
|
||||
setIsSaving(true);
|
||||
|
||||
await sleep(250);
|
||||
await updateConfig(
|
||||
configName,
|
||||
(previousConfig) => ({
|
||||
...previousConfig,
|
||||
settings: {
|
||||
...previousConfig.settings,
|
||||
customization: {
|
||||
...previousConfig.settings.customization,
|
||||
gridstack: values,
|
||||
},
|
||||
},
|
||||
}),
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
form.resetDirty();
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} style={{ position: 'relative' }}>
|
||||
<LoadingOverlay overlayBlur={2} visible={isSaving} radius="md" />
|
||||
<Input.Wrapper
|
||||
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.small') })}
|
||||
description={t('columnsCount.descriptionPreset', { pixels: GridstackBreakpoints.medium })}
|
||||
mb="md"
|
||||
>
|
||||
<Slider min={1} max={8} mt="xs" {...form.getInputProps('columnCountSmall')} />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.medium') })}
|
||||
description={t('columnsCount.descriptionPreset', { pixels: GridstackBreakpoints.large })}
|
||||
mb="md"
|
||||
>
|
||||
<Slider min={3} max={16} mt="xs" {...form.getInputProps('columnCountMedium')} />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.large') })}
|
||||
description={t('columnsCount.descriptionExceedsPreset', {
|
||||
pixels: GridstackBreakpoints.large,
|
||||
})}
|
||||
>
|
||||
<Slider min={5} max={20} mt="xs" {...form.getInputProps('columnCountLarge')} />
|
||||
</Input.Wrapper>
|
||||
{form.isDirty() && (
|
||||
<Alert variant="light" color="yellow" title="Unsaved changes" my="md">
|
||||
{t('unsavedChanges')}
|
||||
</Alert>
|
||||
)}
|
||||
<Grid mt="md">
|
||||
<Grid.Col md={6} xs={12}>
|
||||
<Button variant="light" leftIcon={<IconCheck size={18} />} type="submit" fullWidth>
|
||||
{t('applyChanges')}
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
<Grid.Col md={6} xs={12}>
|
||||
<Button
|
||||
variant="light"
|
||||
leftIcon={<IconReload size={18} />}
|
||||
onClick={() =>
|
||||
form.setValues({
|
||||
columnCountSmall: 3,
|
||||
columnCountMedium: 6,
|
||||
columnCountLarge: 12,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
{t('defaultValues')}
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +1,39 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Checkbox,
|
||||
createStyles,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Indicator,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { createDummyArray } from '../../../../tools/client/arrays';
|
||||
import { CustomizationSettingsType } from '../../../../types/settings';
|
||||
import { Logo } from '../../../layout/Logo';
|
||||
|
||||
interface LayoutSelectorProps {
|
||||
defaultLayout: CustomizationSettingsType['layout'] | undefined;
|
||||
}
|
||||
|
||||
// TODO: add translations
|
||||
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
||||
export const LayoutSelector = () => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
|
||||
const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true);
|
||||
const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false);
|
||||
const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false);
|
||||
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
|
||||
const layoutSettings = config?.settings.customization.layout;
|
||||
|
||||
const { colors, colorScheme } = useMantineTheme();
|
||||
const [leftSidebar, setLeftSidebar] = useState(layoutSettings?.enabledLeftSidebar ?? true);
|
||||
const [rightSidebar, setRightSidebar] = useState(layoutSettings?.enabledRightSidebar ?? true);
|
||||
const [docker, setDocker] = useState(layoutSettings?.enabledDocker ?? false);
|
||||
const [ping, setPing] = useState(layoutSettings?.enabledPing ?? false);
|
||||
const [searchBar, setSearchBar] = useState(layoutSettings?.enabledSearchbar ?? false);
|
||||
const { t } = useTranslation('settings/common');
|
||||
|
||||
if (!configName) return null;
|
||||
if (!configName || !config) return null;
|
||||
|
||||
const handleChange = (
|
||||
key: keyof CustomizationSettingsType['layout'],
|
||||
@@ -68,99 +64,140 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const enabledPing = layoutSettings?.enabledPing ?? false;
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Title order={6}>{t('layout.title')}</Title>
|
||||
|
||||
<Paper px="xs" py={4} withBorder>
|
||||
<Group position="apart">
|
||||
<Logo size="xs" />
|
||||
<Group spacing={5}>
|
||||
{searchBar ? (
|
||||
<Paper
|
||||
style={{
|
||||
height: 10,
|
||||
backgroundColor: colorScheme === 'dark' ? colors.gray[8] : colors.gray[1],
|
||||
}}
|
||||
p={2}
|
||||
w={60}
|
||||
/>
|
||||
) : null}
|
||||
{docker ? <ActionIcon size={10} disabled /> : null}
|
||||
<>
|
||||
<Stack spacing={0} mb="md">
|
||||
<Title order={6}>{t('layout.preview.title')}</Title>
|
||||
<Text color="dimmed" size="xs">
|
||||
{t('layout.preview.subtitle')}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack spacing="xs">
|
||||
<Paper px="xs" py={4} withBorder>
|
||||
<Group position="apart">
|
||||
<Logo size="xs" />
|
||||
<Group spacing={5}>
|
||||
{searchBar && <PlaceholderElement width={60} height={10} />}
|
||||
{docker && <PlaceholderElement width={10} height={10} />}
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Group align="stretch">
|
||||
{leftSidebar && (
|
||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||
<Flex align="center" justify="center" direction="column">
|
||||
<Text align="center">{t('layout.sidebar')}</Text>
|
||||
<Text color="dimmed" size="xs" align="center">
|
||||
Only for
|
||||
<br />
|
||||
apps &<br />
|
||||
integrations
|
||||
</Text>
|
||||
</Flex>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Paper className={classes.primaryWrapper} p="xs" withBorder>
|
||||
<Text align="center">{t('layout.main')}</Text>
|
||||
<Text color="dimmed" size="xs" align="center">
|
||||
{t('layout.cannotturnoff')}
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
{rightSidebar && (
|
||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||
<Flex align="center" justify="center" direction="column">
|
||||
<Text align="center">{t('layout.sidebar')}</Text>
|
||||
<Text color="dimmed" size="xs" align="center">
|
||||
Only for
|
||||
<br />
|
||||
apps &<br />
|
||||
integrations
|
||||
</Text>
|
||||
<Flex gap={6}>
|
||||
{leftSidebar && (
|
||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||
<Flex gap={5} wrap="wrap">
|
||||
{createDummyArray(5).map((item, index) => (
|
||||
<PlaceholderElement
|
||||
height={index % 4 === 0 ? 60 + 5 : 30}
|
||||
width={30}
|
||||
key={`example-item-right-sidebard-${index}`}
|
||||
index={index}
|
||||
hasPing={enabledPing}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Paper className={classes.primaryWrapper} p="xs" withBorder>
|
||||
<Flex gap={5} wrap="wrap">
|
||||
{createDummyArray(10).map((item, index) => (
|
||||
<PlaceholderElement
|
||||
height={30}
|
||||
width={index % 5 === 0 ? 60 : 30}
|
||||
key={`example-item-main-${index}`}
|
||||
index={index}
|
||||
hasPing={enabledPing}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Paper>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Stack spacing="xs">
|
||||
<Checkbox
|
||||
label={t('layout.enablelsidebar')}
|
||||
description={t('layout.enablelsidebardesc')}
|
||||
checked={leftSidebar}
|
||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enablersidebar')}
|
||||
description={t('layout.enablersidebardesc')}
|
||||
checked={rightSidebar}
|
||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enablesearchbar')}
|
||||
checked={searchBar}
|
||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enabledocker')}
|
||||
checked={docker}
|
||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enableping')}
|
||||
checked={ping}
|
||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||
/>
|
||||
{rightSidebar && (
|
||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||
<Flex gap={5} align="start" wrap="wrap">
|
||||
{createDummyArray(5).map((item, index) => (
|
||||
<PlaceholderElement
|
||||
height={30}
|
||||
width={index % 4 === 0 ? 60 + 5 : 30}
|
||||
key={`example-item-right-sidebard-${index}`}
|
||||
index={index}
|
||||
hasPing={enabledPing}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Paper>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Divider label={t('layout.divider')} labelPosition="center" mt="md" mb="xs" />
|
||||
<Stack spacing="xs">
|
||||
<Checkbox
|
||||
label={t('layout.enablelsidebar')}
|
||||
description={t('layout.enablelsidebardesc')}
|
||||
checked={leftSidebar}
|
||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enablersidebar')}
|
||||
description={t('layout.enablersidebardesc')}
|
||||
checked={rightSidebar}
|
||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enablesearchbar')}
|
||||
checked={searchBar}
|
||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enabledocker')}
|
||||
checked={docker}
|
||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enableping')}
|
||||
checked={ping}
|
||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BaseElement = ({ height, width }: { height: number; width: number }) => (
|
||||
<Paper
|
||||
sx={(theme) => ({
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.gray[8] : theme.colors.gray[1],
|
||||
})}
|
||||
h={height}
|
||||
p={2}
|
||||
w={width}
|
||||
/>
|
||||
);
|
||||
|
||||
const PlaceholderElement = (props: any) => {
|
||||
const { height, width, hasPing, index } = props;
|
||||
|
||||
if (hasPing) {
|
||||
return (
|
||||
<Indicator
|
||||
position="bottom-end"
|
||||
size={5}
|
||||
offset={10}
|
||||
color={index % 4 === 0 ? 'red' : 'green'}
|
||||
>
|
||||
<BaseElement width={width} height={height} />
|
||||
</Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseElement width={width} height={height} />;
|
||||
};
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
primaryWrapper: {
|
||||
flexGrow: 2,
|
||||
|
||||
@@ -4,15 +4,13 @@ import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
|
||||
interface BackgroundChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const BackgroundChanger = ({ defaultValue }: BackgroundChangerProps) => {
|
||||
export const BackgroundChanger = () => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [backgroundImageUrl, setBackgroundImageUrl] = useState(defaultValue);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const [backgroundImageUrl, setBackgroundImageUrl] = useState(
|
||||
config?.settings.customization.backgroundImageUrl
|
||||
);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
|
||||
@@ -4,21 +4,19 @@ import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
|
||||
interface FaviconChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => {
|
||||
export const FaviconChanger = () => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [faviconUrl, setFaviconUrl] = useState(defaultValue);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const [faviconUrl, setFaviconUrl] = useState(
|
||||
config?.settings.customization.faviconUrl ?? '/imgs/favicon/favicon-squared.png'
|
||||
);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const { value } = ev.currentTarget;
|
||||
const faviconUrl = value.trim().length === 0 ? undefined : value;
|
||||
const faviconUrl = value.trim();
|
||||
setFaviconUrl(faviconUrl);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
@@ -35,6 +33,7 @@ export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => {
|
||||
return (
|
||||
<TextInput
|
||||
label={t('favicon.label')}
|
||||
description={t('favicon.description')}
|
||||
placeholder="/imgs/favicon/favicon.svg"
|
||||
value={faviconUrl}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -4,21 +4,17 @@ import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
|
||||
interface LogoImageChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => {
|
||||
export const LogoImageChanger = () => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [logoImageSrc, setLogoImageSrc] = useState(defaultValue);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const [logoImageSrc, setLogoImageSrc] = useState(config?.settings.customization.logoImageUrl ?? '/imgs/logo/logo.png');
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const { value } = ev.currentTarget;
|
||||
const logoImageSrc = value.trim().length === 0 ? undefined : value;
|
||||
const logoImageSrc = value.trim();
|
||||
setLogoImageSrc(logoImageSrc);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
@@ -35,9 +31,11 @@ export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => {
|
||||
return (
|
||||
<TextInput
|
||||
label={t('logo.label')}
|
||||
description={t('logo.description')}
|
||||
placeholder="/imgs/logo/logo.png"
|
||||
value={logoImageSrc}
|
||||
onChange={handleChange}
|
||||
mb="sm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,22 +4,17 @@ import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
|
||||
interface MetaTitleChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
// TODO: change to pageTitle
|
||||
export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => {
|
||||
export const BrowserTabTitle = () => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [metaTitle, setMetaTitle] = useState(defaultValue);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const [metaTitle, setMetaTitle] = useState(config?.settings.customization.metaTitle ?? '');
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const { value } = ev.currentTarget;
|
||||
const metaTitle = value.trim().length === 0 ? undefined : value;
|
||||
const metaTitle = value.trim();
|
||||
setMetaTitle(metaTitle);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
@@ -36,9 +31,11 @@ export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => {
|
||||
return (
|
||||
<TextInput
|
||||
label={t('metaTitle.label')}
|
||||
description={t('metaTitle.description')}
|
||||
placeholder="homarr - the best dashboard"
|
||||
value={metaTitle}
|
||||
onChange={handleChange}
|
||||
mb="sm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,22 +4,17 @@ import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
|
||||
interface PageTitleChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
// TODO: change to dashboard title
|
||||
export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => {
|
||||
export const DashboardTitleChanger = () => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [pageTitle, setPageTitle] = useState(defaultValue);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const [pageTitle, setPageTitle] = useState(config?.settings.customization.pageTitle ?? '');
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const { value } = ev.currentTarget;
|
||||
const pageTitle = value.trim().length === 0 ? undefined : value;
|
||||
const pageTitle = value.trim();
|
||||
setPageTitle(pageTitle);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
@@ -36,9 +31,11 @@ export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => {
|
||||
return (
|
||||
<TextInput
|
||||
label={t('pageTitle.label')}
|
||||
description={t('pageTitle.description')}
|
||||
placeholder="homarr"
|
||||
value={pageTitle}
|
||||
onChange={handleChange}
|
||||
mb="sm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ColorSelector({ type, defaultValue }: ColorControlProps) {
|
||||
const [color, setColor] = useState(defaultValue);
|
||||
const [popoverOpened, popover] = useDisclosure(false);
|
||||
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
|
||||
const { name: configName } = useConfigContext();
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const theme = useMantineTheme();
|
||||
|
||||
@@ -1,44 +1,105 @@
|
||||
import { Textarea } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
createStyles,
|
||||
Group,
|
||||
Loader,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
|
||||
interface CustomCssChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
const CodeEditor = dynamic(
|
||||
() => import('@uiw/react-textarea-code-editor').then((mod) => mod.default),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export const CustomCssChanger = ({ defaultValue }: CustomCssChangerProps) => {
|
||||
export const CustomCssChanger = () => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [customCss, setCustomCss] = useState(defaultValue);
|
||||
const { colorScheme, colors } = useMantineTheme();
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const [nonDebouncedCustomCSS, setNonDebouncedCustomCSS] = useState(
|
||||
config?.settings.customization.customCss ?? ''
|
||||
);
|
||||
const [debouncedCustomCSS] = useDebouncedValue(nonDebouncedCustomCSS, 696);
|
||||
const { classes } = useStyles();
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (ev) => {
|
||||
const { value } = ev.currentTarget;
|
||||
const customCss = value.trim().length === 0 ? undefined : value;
|
||||
setCustomCss(customCss);
|
||||
useEffect(() => {
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
customCss,
|
||||
customCss: debouncedCustomCSS,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
}, [debouncedCustomCSS]);
|
||||
|
||||
const codeIsDirty = nonDebouncedCustomCSS !== debouncedCustomCSS;
|
||||
const codeEditorHeight = codeIsDirty ? 250 - 42 : 250;
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
minRows={5}
|
||||
label={t('customCSS.label')}
|
||||
placeholder={t('customCSS.placeholder')}
|
||||
value={customCss}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Stack spacing={4} mt="xl">
|
||||
<Text>{t('customCSS.label')}</Text>
|
||||
<Text color="dimmed" size="xs">
|
||||
{t('customCSS.description')}
|
||||
</Text>
|
||||
<div className={classes.codeEditorRoot}>
|
||||
<ScrollArea style={{ height: codeEditorHeight }}>
|
||||
<CodeEditor
|
||||
className={classes.codeEditor}
|
||||
placeholder={t('customCSS.placeholder')}
|
||||
value={nonDebouncedCustomCSS}
|
||||
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setNonDebouncedCustomCSS(event.target.value.trim())
|
||||
}
|
||||
language="css"
|
||||
data-color-mode={colorScheme}
|
||||
minHeight={codeEditorHeight}
|
||||
/>
|
||||
</ScrollArea>
|
||||
{codeIsDirty && (
|
||||
<Box className={classes.codeEditorFooter}>
|
||||
<Group p="xs" spacing="xs">
|
||||
<Loader color={colors.gray[0]} size={18} />
|
||||
<Text>{t('customCSS.applying')}</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ colors, colorScheme, radius }) => ({
|
||||
codeEditorFooter: {
|
||||
borderBottomLeftRadius: radius.sm,
|
||||
borderBottomRightRadius: radius.sm,
|
||||
backgroundColor: colorScheme === 'dark' ? colors.dark[7] : undefined,
|
||||
},
|
||||
codeEditorRoot: {
|
||||
borderColor: colorScheme === 'dark' ? colors.dark[4] : colors.gray[4],
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
codeEditor: {
|
||||
backgroundColor: colorScheme === 'dark' ? colors.dark[6] : 'white',
|
||||
fontSize: 12,
|
||||
|
||||
'& ::placeholder': {
|
||||
color: colorScheme === 'dark' ? colors.dark[3] : colors.gray[5],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -4,15 +4,11 @@ import { useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
|
||||
interface OpacitySelectorProps {
|
||||
defaultValue: number | undefined;
|
||||
}
|
||||
|
||||
export function OpacitySelector({ defaultValue }: OpacitySelectorProps) {
|
||||
const [opacity, setOpacity] = useState(defaultValue || 100);
|
||||
export function DashboardTilesOpacitySelector() {
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const [opacity, setOpacity] = useState(config?.settings.customization.appOpacity || 100);
|
||||
const { t } = useTranslation('settings/customization/opacity-selector');
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
@@ -15,17 +15,13 @@ import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { useColorTheme } from '../../../../tools/color';
|
||||
|
||||
interface ShadeSelectorProps {
|
||||
defaultValue: MantineTheme['primaryShade'] | undefined;
|
||||
}
|
||||
|
||||
export function ShadeSelector({ defaultValue }: ShadeSelectorProps) {
|
||||
export function ShadeSelector() {
|
||||
const { t } = useTranslation('settings/customization/shade-selector');
|
||||
const [shade, setShade] = useState(defaultValue);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const [shade, setShade] = useState(config?.settings.customization.colors.shade);
|
||||
const [popoverOpened, popover] = useDisclosure(false);
|
||||
const { primaryColor, setPrimaryShade } = useColorTheme();
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const theme = useMantineTheme();
|
||||
|
||||
@@ -17,8 +17,11 @@ import { useCardStyles } from '../../../useCardStyles';
|
||||
export const ToggleEditModeAction = () => {
|
||||
const { enabled, toggleEditMode } = useEditModeStore();
|
||||
const namedWrapperColumnCount = useNamedWrapperColumnCount();
|
||||
const { t } = useTranslation('layout/header/actions/toggle-edit-mode');
|
||||
const translatedSize = t(`screenSizes.${namedWrapperColumnCount}`);
|
||||
const { t } = useTranslation(['layout/header/actions/toggle-edit-mode', 'common']);
|
||||
const translatedSize =
|
||||
namedWrapperColumnCount !== null
|
||||
? t(`common:breakPoints.${namedWrapperColumnCount}`)
|
||||
: t('common:loading');
|
||||
|
||||
const smallerThanSm = useScreenSmallerThan('sm');
|
||||
const { config } = useConfigContext();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { REPO_URL } from '../../../../data/constants';
|
||||
import DockerMenuButton from '../../../modules/Docker/DockerModule';
|
||||
import { usePackageAttributesStore } from '../../../tools/client/zustands/usePackageAttributesStore';
|
||||
|
||||
4
src/constants/gridstack-breakpoints.ts
Normal file
4
src/constants/gridstack-breakpoints.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const GridstackBreakpoints = {
|
||||
large: 1400,
|
||||
medium: 768,
|
||||
};
|
||||
@@ -18,7 +18,6 @@ import { WidgetsEditModal } from '../components/Dashboard/Tiles/Widgets/WidgetsE
|
||||
import { WidgetsRemoveModal } from '../components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
|
||||
import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal';
|
||||
import { ConfigProvider } from '../config/provider';
|
||||
import '../styles/global.scss';
|
||||
import { ColorTheme } from '../tools/color';
|
||||
import { queryClient } from '../tools/queryClient';
|
||||
import { theme } from '../tools/theme';
|
||||
@@ -28,6 +27,9 @@ import {
|
||||
} from '../tools/server/getPackageVersion';
|
||||
import { usePackageAttributesStore } from '../tools/client/zustands/usePackageAttributesStore';
|
||||
|
||||
import '../styles/global.scss';
|
||||
import '@uiw/react-textarea-code-editor/dist.css';
|
||||
|
||||
function App(
|
||||
this: any,
|
||||
props: AppProps & { colorScheme: ColorScheme; packageAttributes: ServerSidePackageAttributesType }
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
// Styling for grid-stack main area
|
||||
@for $i from 1 to 13 {
|
||||
@for $i from 1 to 21 {
|
||||
.grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) }
|
||||
.grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) }
|
||||
.grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) }
|
||||
@@ -30,7 +30,7 @@
|
||||
.grid-stack>.grid-stack-item[gs-max-h="#{$i}"] { max-height: calc(#{$i}px * #{var(--gridstack-widget-width)}) }
|
||||
}
|
||||
|
||||
@for $i from 1 to 13 {
|
||||
@for $i from 1 to 21 {
|
||||
.grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: calc(100% / #{var(--gridstack-column-count)} * #{$i}) }
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-max-h="#{$i}"] { max-height: 128px * $i }
|
||||
}
|
||||
|
||||
@for $i from 1 to 13 {
|
||||
@for $i from 1 to 3 {
|
||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-x="#{$i}"] { left: 128px * $i }
|
||||
}
|
||||
|
||||
@@ -91,4 +91,4 @@
|
||||
.gridstack-empty-wrapper {
|
||||
height: 0px;
|
||||
min-height: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
1
src/tools/client/arrays.ts
Normal file
1
src/tools/client/arrays.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const createDummyArray = (countItems: number) => Array.from(Array(countItems).keys());
|
||||
4
src/tools/client/time.ts
Normal file
4
src/tools/client/time.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const sleep = (ms: number) =>
|
||||
new Promise((r) => {
|
||||
setTimeout(r, ms);
|
||||
});
|
||||
@@ -13,11 +13,13 @@ export const dashboardNamespaces = [
|
||||
'settings/general/internationalization',
|
||||
'settings/general/search-engine',
|
||||
'settings/general/widget-positions',
|
||||
'settings/customization/general',
|
||||
'settings/customization/color-selector',
|
||||
'settings/customization/page-appearance',
|
||||
'settings/customization/shade-selector',
|
||||
'settings/customization/app-width',
|
||||
'settings/customization/opacity-selector',
|
||||
'settings/customization/gridstack',
|
||||
'modules/common',
|
||||
'modules/date',
|
||||
'modules/calendar',
|
||||
|
||||
@@ -44,6 +44,13 @@ export interface CustomizationSettingsType {
|
||||
customCss?: string;
|
||||
colors: ColorsCustomizationSettingsType;
|
||||
appOpacity?: number;
|
||||
gridstack?: GridstackSettingsType;
|
||||
}
|
||||
|
||||
export interface GridstackSettingsType {
|
||||
columnCountSmall: number; // default: 3
|
||||
columnCountMedium: number; // default: 6
|
||||
columnCountLarge: number; // default: 12
|
||||
}
|
||||
|
||||
interface LayoutCustomizationSettingsType {
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Indicator,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
@@ -41,7 +40,7 @@ const definition = defineWidget({
|
||||
component: TorrentNetworkTrafficTile,
|
||||
});
|
||||
|
||||
export type ITorrentNetworkTraffic = IWidget<typeof definition['id'], typeof definition>;
|
||||
export type ITorrentNetworkTraffic = IWidget<(typeof definition)['id'], typeof definition>;
|
||||
|
||||
interface TorrentNetworkTrafficTileProps {
|
||||
widget: ITorrentNetworkTraffic;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TorrentState } from '@ctrl/shared-torrent';
|
||||
import {
|
||||
Badge,
|
||||
Center,
|
||||
@@ -52,7 +51,7 @@ const definition = defineWidget({
|
||||
component: TorrentTile,
|
||||
});
|
||||
|
||||
export type ITorrent = IWidget<typeof definition['id'], typeof definition>;
|
||||
export type ITorrent = IWidget<(typeof definition)['id'], typeof definition>;
|
||||
|
||||
interface TorrentTileProps {
|
||||
widget: ITorrent;
|
||||
|
||||
Reference in New Issue
Block a user