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 { i18n } = require('./next-i18next.config');
|
||||||
|
|
||||||
|
const removeImports = require('next-remove-imports')();
|
||||||
|
|
||||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer(
|
||||||
images: {
|
removeImports({
|
||||||
domains: ['cdn.jsdelivr.net'],
|
experimental: { esmExternals: true },
|
||||||
},
|
images: {
|
||||||
reactStrictMode: true,
|
domains: ['cdn.jsdelivr.net'],
|
||||||
output: 'standalone',
|
},
|
||||||
i18n,
|
reactStrictMode: true,
|
||||||
});
|
output: 'standalone',
|
||||||
|
i18n,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"@tabler/icons": "^1.106.0",
|
"@tabler/icons": "^1.106.0",
|
||||||
"@tanstack/react-query": "^4.2.1",
|
"@tanstack/react-query": "^4.2.1",
|
||||||
"@tanstack/react-query-devtools": "^4.24.4",
|
"@tanstack/react-query-devtools": "^4.24.4",
|
||||||
|
"@uiw/react-textarea-code-editor": "v1.4.4",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"consola": "^2.15.3",
|
"consola": "^2.15.3",
|
||||||
"cookies-next": "^2.1.1",
|
"cookies-next": "^2.1.1",
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"next": "^13.1.6",
|
"next": "^13.1.6",
|
||||||
"next-i18next": "^11.3.0",
|
"next-i18next": "^11.3.0",
|
||||||
|
"next-remove-imports": "^1.0.8",
|
||||||
"nzbget-api": "^0.0.3",
|
"nzbget-api": "^0.0.3",
|
||||||
"ping": "^0.4.2",
|
"ping": "^0.4.2",
|
||||||
"prism-react-renderer": "^1.3.5",
|
"prism-react-renderer": "^1.3.5",
|
||||||
@@ -81,6 +83,8 @@
|
|||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
||||||
"@typescript-eslint/parser": "^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": "^8.20.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
|
|||||||
@@ -24,5 +24,11 @@
|
|||||||
"seconds": "seconds",
|
"seconds": "seconds",
|
||||||
"minutes": "minutes",
|
"minutes": "minutes",
|
||||||
"hours": "hours"
|
"hours": "hours"
|
||||||
|
},
|
||||||
|
"loading": "Loading...",
|
||||||
|
"breakPoints": {
|
||||||
|
"small": "small",
|
||||||
|
"medium": "medium",
|
||||||
|
"large": "large"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,5 @@
|
|||||||
"popover": {
|
"popover": {
|
||||||
"title": "Edit mode is enabled for <1>{{size}}</1> size",
|
"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"
|
"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.",
|
"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!",
|
"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)",
|
"grow": "Grow grid (take all space)",
|
||||||
"layout": {
|
"layout": {
|
||||||
"title": "Dashboard layout",
|
"preview": {
|
||||||
|
"title": "Preview",
|
||||||
|
"subtitle": "Changes will be saved automatically"
|
||||||
|
},
|
||||||
|
"divider": "Layout options",
|
||||||
"main": "Main",
|
"main": "Main",
|
||||||
"sidebar": "Sidebar",
|
"sidebar": "Sidebar",
|
||||||
"cannotturnoff": "Cannot be turned off",
|
"cannotturnoff": "Cannot be turned off",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
|
"colors": "Colors",
|
||||||
"suffix": "{{color}} color"
|
"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": {
|
"pageTitle": {
|
||||||
"label": "Page Title"
|
"label": "Page Title",
|
||||||
|
"description": "The dashboard title at the top left"
|
||||||
},
|
},
|
||||||
"metaTitle": {
|
"metaTitle": {
|
||||||
"label": "Meta Title"
|
"label": "Meta Title",
|
||||||
|
"description": "The title, that is being displayed as your tab name"
|
||||||
},
|
},
|
||||||
"logo": {
|
"logo": {
|
||||||
"label": "Logo"
|
"label": "Logo",
|
||||||
|
"description": "The dashboard logo at the top left"
|
||||||
},
|
},
|
||||||
"favicon": {
|
"favicon": {
|
||||||
"label": "Favicon"
|
"label": "Favicon",
|
||||||
|
"description": "The icon, that is being used in front of your tab name"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"label": "Background"
|
"label": "Background"
|
||||||
},
|
},
|
||||||
"customCSS": {
|
"customCSS": {
|
||||||
"label": "Custom CSS",
|
"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": {
|
"buttons": {
|
||||||
"submit": "Submit"
|
"submit": "Submit"
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
|||||||
<ActionIcon className={classes.informationIcon} variant="default">
|
<ActionIcon className={classes.informationIcon} variant="default">
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
{t(item.label)}
|
{t(`layout/modals/about:metrics.${item.label}`)}
|
||||||
</Group>
|
</Group>
|
||||||
</td>
|
</td>
|
||||||
<td className={classes.informationTableColumn}>{item.content}</td>
|
<td className={classes.informationTableColumn}>{item.content}</td>
|
||||||
@@ -151,7 +151,6 @@ interface ExtendedInitOptions extends InitOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
||||||
// TODO: Fix this to not request. Pass it as a prop.
|
|
||||||
const colorGradiant = usePrimaryGradient();
|
const colorGradiant = usePrimaryGradient();
|
||||||
const { attributes } = usePackageAttributesStore();
|
const { attributes } = usePackageAttributesStore();
|
||||||
|
|
||||||
@@ -168,7 +167,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
...items,
|
...items,
|
||||||
{
|
{
|
||||||
icon: <IconLanguage size={20} />,
|
icon: <IconLanguage size={20} />,
|
||||||
label: 'layout/modals/about:i18n',
|
label: 'i18n',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
{usedI18nNamespaces.length}
|
{usedI18nNamespaces.length}
|
||||||
@@ -177,7 +176,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <IconVocabulary size={20} />,
|
icon: <IconVocabulary size={20} />,
|
||||||
label: 'layout/modals/about:locales',
|
label: 'locales',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
{initOptions.locales.length}
|
{initOptions.locales.length}
|
||||||
@@ -190,7 +189,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
icon: <IconSchema size={20} />,
|
icon: <IconSchema size={20} />,
|
||||||
label: 'Configuration schema version',
|
label: 'configurationSchemaVersion',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
{configVersion}
|
{configVersion}
|
||||||
@@ -199,7 +198,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <IconFile size={20} />,
|
icon: <IconFile size={20} />,
|
||||||
label: 'Available configurations',
|
label: 'configurationsCount',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
{configs.length}
|
{configs.length}
|
||||||
@@ -249,7 +248,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <IconAnchor size={20} />,
|
icon: <IconAnchor size={20} />,
|
||||||
label: 'Node environment',
|
label: 'nodeEnvironment',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
{attributes.environment}
|
{attributes.environment}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useMantineTheme } from '@mantine/core';
|
import { useMantineTheme } from '@mantine/core';
|
||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
import { GridstackBreakpoints } from '../../../../constants/gridstack-breakpoints';
|
||||||
|
|
||||||
export const useGridstackStore = create<GridstackStoreType>((set, get) => ({
|
export const useGridstackStore = create<GridstackStoreType>((set, get) => ({
|
||||||
mainAreaWidth: null,
|
mainAreaWidth: null,
|
||||||
@@ -27,18 +29,28 @@ export const useNamedWrapperColumnCount = (): 'small' | 'medium' | 'large' | nul
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useWrapperColumnCount = () => {
|
export const useWrapperColumnCount = () => {
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
switch (useNamedWrapperColumnCount()) {
|
switch (useNamedWrapperColumnCount()) {
|
||||||
case 'large':
|
case 'large':
|
||||||
return 12;
|
return config.settings.customization.gridstack?.columnCountLarge ?? 12;
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return 6;
|
return config.settings.customization.gridstack?.columnCountMedium ?? 6;
|
||||||
case 'small':
|
case 'small':
|
||||||
return 3;
|
return config.settings.customization.gridstack?.columnCountSmall ?? 3;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCurrentShapeSize(size: number) {
|
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 { useViewportSize } from '@mantine/hooks';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { CustomizationSettingsAccordeon } from './CustomizationAccordeon';
|
||||||
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';
|
|
||||||
|
|
||||||
export default function CustomizationSettings() {
|
export default function CustomizationSettings() {
|
||||||
const { config, name: configName } = useConfigContext();
|
const { height } = useViewportSize();
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('settings/customization/general');
|
||||||
const { height, width } = useViewportSize();
|
|
||||||
|
|
||||||
const { updateConfig } = useConfigStore();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
||||||
<Stack mt="xs" mb="md" spacing="xs">
|
<Stack mt="xs" mb="md" spacing="xs">
|
||||||
<LayoutSelector defaultLayout={config?.settings.customization.layout} />
|
<Text color="dimmed">
|
||||||
<PageTitleChanger defaultValue={config?.settings.customization.pageTitle} />
|
{t('text')}
|
||||||
<MetaTitleChanger defaultValue={config?.settings.customization.metaTitle} />
|
</Text>
|
||||||
<LogoImageChanger defaultValue={config?.settings.customization.logoImageUrl} />
|
<CustomizationSettingsAccordeon />
|
||||||
<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} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</ScrollArea>
|
</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 {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
createStyles,
|
createStyles,
|
||||||
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
|
Indicator,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
import { createDummyArray } from '../../../../tools/client/arrays';
|
||||||
import { CustomizationSettingsType } from '../../../../types/settings';
|
import { CustomizationSettingsType } from '../../../../types/settings';
|
||||||
import { Logo } from '../../../layout/Logo';
|
import { Logo } from '../../../layout/Logo';
|
||||||
|
|
||||||
interface LayoutSelectorProps {
|
export const LayoutSelector = () => {
|
||||||
defaultLayout: CustomizationSettingsType['layout'] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add translations
|
|
||||||
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
|
||||||
const { name: configName } = useConfigContext();
|
const { config, name: configName } = useConfigContext();
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
|
||||||
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
|
const layoutSettings = config?.settings.customization.layout;
|
||||||
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 { 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');
|
const { t } = useTranslation('settings/common');
|
||||||
|
|
||||||
if (!configName) return null;
|
if (!configName || !config) return null;
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
key: keyof CustomizationSettingsType['layout'],
|
key: keyof CustomizationSettingsType['layout'],
|
||||||
@@ -68,99 +64,140 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const enabledPing = layoutSettings?.enabledPing ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing="xs">
|
<>
|
||||||
<Title order={6}>{t('layout.title')}</Title>
|
<Stack spacing={0} mb="md">
|
||||||
|
<Title order={6}>{t('layout.preview.title')}</Title>
|
||||||
<Paper px="xs" py={4} withBorder>
|
<Text color="dimmed" size="xs">
|
||||||
<Group position="apart">
|
{t('layout.preview.subtitle')}
|
||||||
<Logo size="xs" />
|
</Text>
|
||||||
<Group spacing={5}>
|
</Stack>
|
||||||
{searchBar ? (
|
<Stack spacing="xs">
|
||||||
<Paper
|
<Paper px="xs" py={4} withBorder>
|
||||||
style={{
|
<Group position="apart">
|
||||||
height: 10,
|
<Logo size="xs" />
|
||||||
backgroundColor: colorScheme === 'dark' ? colors.gray[8] : colors.gray[1],
|
<Group spacing={5}>
|
||||||
}}
|
{searchBar && <PlaceholderElement width={60} height={10} />}
|
||||||
p={2}
|
{docker && <PlaceholderElement width={10} height={10} />}
|
||||||
w={60}
|
</Group>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{docker ? <ActionIcon size={10} disabled /> : null}
|
|
||||||
</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>
|
</Paper>
|
||||||
|
|
||||||
{rightSidebar && (
|
<Flex gap={6}>
|
||||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
{leftSidebar && (
|
||||||
<Flex align="center" justify="center" direction="column">
|
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||||
<Text align="center">{t('layout.sidebar')}</Text>
|
<Flex gap={5} wrap="wrap">
|
||||||
<Text color="dimmed" size="xs" align="center">
|
{createDummyArray(5).map((item, index) => (
|
||||||
Only for
|
<PlaceholderElement
|
||||||
<br />
|
height={index % 4 === 0 ? 60 + 5 : 30}
|
||||||
apps &<br />
|
width={30}
|
||||||
integrations
|
key={`example-item-right-sidebard-${index}`}
|
||||||
</Text>
|
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>
|
</Flex>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Stack spacing="xs">
|
{rightSidebar && (
|
||||||
<Checkbox
|
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||||
label={t('layout.enablelsidebar')}
|
<Flex gap={5} align="start" wrap="wrap">
|
||||||
description={t('layout.enablelsidebardesc')}
|
{createDummyArray(5).map((item, index) => (
|
||||||
checked={leftSidebar}
|
<PlaceholderElement
|
||||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
height={30}
|
||||||
/>
|
width={index % 4 === 0 ? 60 + 5 : 30}
|
||||||
<Checkbox
|
key={`example-item-right-sidebard-${index}`}
|
||||||
label={t('layout.enablersidebar')}
|
index={index}
|
||||||
description={t('layout.enablersidebardesc')}
|
hasPing={enabledPing}
|
||||||
checked={rightSidebar}
|
/>
|
||||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
))}
|
||||||
/>
|
</Flex>
|
||||||
<Checkbox
|
</Paper>
|
||||||
label={t('layout.enablesearchbar')}
|
)}
|
||||||
checked={searchBar}
|
</Flex>
|
||||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
|
||||||
/>
|
<Divider label={t('layout.divider')} labelPosition="center" mt="md" mb="xs" />
|
||||||
<Checkbox
|
<Stack spacing="xs">
|
||||||
label={t('layout.enabledocker')}
|
<Checkbox
|
||||||
checked={docker}
|
label={t('layout.enablelsidebar')}
|
||||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
description={t('layout.enablelsidebardesc')}
|
||||||
/>
|
checked={leftSidebar}
|
||||||
<Checkbox
|
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||||
label={t('layout.enableping')}
|
/>
|
||||||
checked={ping}
|
<Checkbox
|
||||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
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>
|
||||||
</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) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
primaryWrapper: {
|
primaryWrapper: {
|
||||||
flexGrow: 2,
|
flexGrow: 2,
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ import { ChangeEventHandler, useState } from 'react';
|
|||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface BackgroundChangerProps {
|
export const BackgroundChanger = () => {
|
||||||
defaultValue: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BackgroundChanger = ({ defaultValue }: BackgroundChangerProps) => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
const { t } = useTranslation('settings/customization/page-appearance');
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
const { name: configName } = useConfigContext();
|
const { config, name: configName } = useConfigContext();
|
||||||
const [backgroundImageUrl, setBackgroundImageUrl] = useState(defaultValue);
|
const [backgroundImageUrl, setBackgroundImageUrl] = useState(
|
||||||
|
config?.settings.customization.backgroundImageUrl
|
||||||
|
);
|
||||||
|
|
||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,19 @@ import { ChangeEventHandler, useState } from 'react';
|
|||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface FaviconChangerProps {
|
export const FaviconChanger = () => {
|
||||||
defaultValue: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
const { t } = useTranslation('settings/customization/page-appearance');
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
const { name: configName } = useConfigContext();
|
const { config, name: configName } = useConfigContext();
|
||||||
const [faviconUrl, setFaviconUrl] = useState(defaultValue);
|
const [faviconUrl, setFaviconUrl] = useState(
|
||||||
|
config?.settings.customization.faviconUrl ?? '/imgs/favicon/favicon-squared.png'
|
||||||
|
);
|
||||||
|
|
||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const { value } = ev.currentTarget;
|
const { value } = ev.currentTarget;
|
||||||
const faviconUrl = value.trim().length === 0 ? undefined : value;
|
const faviconUrl = value.trim();
|
||||||
setFaviconUrl(faviconUrl);
|
setFaviconUrl(faviconUrl);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -35,6 +33,7 @@ export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('favicon.label')}
|
label={t('favicon.label')}
|
||||||
|
description={t('favicon.description')}
|
||||||
placeholder="/imgs/favicon/favicon.svg"
|
placeholder="/imgs/favicon/favicon.svg"
|
||||||
value={faviconUrl}
|
value={faviconUrl}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|||||||
@@ -4,21 +4,17 @@ import { ChangeEventHandler, useState } from 'react';
|
|||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface LogoImageChangerProps {
|
export const LogoImageChanger = () => {
|
||||||
defaultValue: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
const { t } = useTranslation('settings/customization/page-appearance');
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
const { name: configName } = useConfigContext();
|
const { config, name: configName } = useConfigContext();
|
||||||
const [logoImageSrc, setLogoImageSrc] = useState(defaultValue);
|
const [logoImageSrc, setLogoImageSrc] = useState(config?.settings.customization.logoImageUrl ?? '/imgs/logo/logo.png');
|
||||||
|
|
||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const { value } = ev.currentTarget;
|
const { value } = ev.currentTarget;
|
||||||
const logoImageSrc = value.trim().length === 0 ? undefined : value;
|
const logoImageSrc = value.trim();
|
||||||
setLogoImageSrc(logoImageSrc);
|
setLogoImageSrc(logoImageSrc);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -35,9 +31,11 @@ export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('logo.label')}
|
label={t('logo.label')}
|
||||||
|
description={t('logo.description')}
|
||||||
placeholder="/imgs/logo/logo.png"
|
placeholder="/imgs/logo/logo.png"
|
||||||
value={logoImageSrc}
|
value={logoImageSrc}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
mb="sm"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,22 +4,17 @@ import { ChangeEventHandler, useState } from 'react';
|
|||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface MetaTitleChangerProps {
|
export const BrowserTabTitle = () => {
|
||||||
defaultValue: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: change to pageTitle
|
|
||||||
export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
const { t } = useTranslation('settings/customization/page-appearance');
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
const { name: configName } = useConfigContext();
|
const { config, name: configName } = useConfigContext();
|
||||||
const [metaTitle, setMetaTitle] = useState(defaultValue);
|
const [metaTitle, setMetaTitle] = useState(config?.settings.customization.metaTitle ?? '');
|
||||||
|
|
||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const { value } = ev.currentTarget;
|
const { value } = ev.currentTarget;
|
||||||
const metaTitle = value.trim().length === 0 ? undefined : value;
|
const metaTitle = value.trim();
|
||||||
setMetaTitle(metaTitle);
|
setMetaTitle(metaTitle);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -36,9 +31,11 @@ export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('metaTitle.label')}
|
label={t('metaTitle.label')}
|
||||||
|
description={t('metaTitle.description')}
|
||||||
placeholder="homarr - the best dashboard"
|
placeholder="homarr - the best dashboard"
|
||||||
value={metaTitle}
|
value={metaTitle}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
mb="sm"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,22 +4,17 @@ import { ChangeEventHandler, useState } from 'react';
|
|||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface PageTitleChangerProps {
|
export const DashboardTitleChanger = () => {
|
||||||
defaultValue: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: change to dashboard title
|
|
||||||
export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
const { t } = useTranslation('settings/customization/page-appearance');
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
const { name: configName } = useConfigContext();
|
const { config, name: configName } = useConfigContext();
|
||||||
const [pageTitle, setPageTitle] = useState(defaultValue);
|
const [pageTitle, setPageTitle] = useState(config?.settings.customization.pageTitle ?? '');
|
||||||
|
|
||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const { value } = ev.currentTarget;
|
const { value } = ev.currentTarget;
|
||||||
const pageTitle = value.trim().length === 0 ? undefined : value;
|
const pageTitle = value.trim();
|
||||||
setPageTitle(pageTitle);
|
setPageTitle(pageTitle);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -36,9 +31,11 @@ export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('pageTitle.label')}
|
label={t('pageTitle.label')}
|
||||||
|
description={t('pageTitle.description')}
|
||||||
placeholder="homarr"
|
placeholder="homarr"
|
||||||
value={pageTitle}
|
value={pageTitle}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
mb="sm"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function ColorSelector({ type, defaultValue }: ColorControlProps) {
|
|||||||
const [color, setColor] = useState(defaultValue);
|
const [color, setColor] = useState(defaultValue);
|
||||||
const [popoverOpened, popover] = useDisclosure(false);
|
const [popoverOpened, popover] = useDisclosure(false);
|
||||||
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
|
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
|
||||||
const { name: configName } = useConfigContext();
|
const { config, name: configName } = useConfigContext();
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
|
||||||
const theme = useMantineTheme();
|
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 { 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 { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface CustomCssChangerProps {
|
const CodeEditor = dynamic(
|
||||||
defaultValue: string | undefined;
|
() => 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 { t } = useTranslation('settings/customization/page-appearance');
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
const { name: configName } = useConfigContext();
|
const { colorScheme, colors } = useMantineTheme();
|
||||||
const [customCss, setCustomCss] = useState(defaultValue);
|
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;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (ev) => {
|
useEffect(() => {
|
||||||
const { value } = ev.currentTarget;
|
|
||||||
const customCss = value.trim().length === 0 ? undefined : value;
|
|
||||||
setCustomCss(customCss);
|
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
customization: {
|
customization: {
|
||||||
...prev.settings.customization,
|
...prev.settings.customization,
|
||||||
customCss,
|
customCss: debouncedCustomCSS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
}, [debouncedCustomCSS]);
|
||||||
|
|
||||||
|
const codeIsDirty = nonDebouncedCustomCSS !== debouncedCustomCSS;
|
||||||
|
const codeEditorHeight = codeIsDirty ? 250 - 42 : 250;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Stack spacing={4} mt="xl">
|
||||||
minRows={5}
|
<Text>{t('customCSS.label')}</Text>
|
||||||
label={t('customCSS.label')}
|
<Text color="dimmed" size="xs">
|
||||||
placeholder={t('customCSS.placeholder')}
|
{t('customCSS.description')}
|
||||||
value={customCss}
|
</Text>
|
||||||
onChange={handleChange}
|
<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 { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface OpacitySelectorProps {
|
export function DashboardTilesOpacitySelector() {
|
||||||
defaultValue: number | undefined;
|
const { config, name: configName } = useConfigContext();
|
||||||
}
|
const [opacity, setOpacity] = useState(config?.settings.customization.appOpacity || 100);
|
||||||
|
|
||||||
export function OpacitySelector({ defaultValue }: OpacitySelectorProps) {
|
|
||||||
const [opacity, setOpacity] = useState(defaultValue || 100);
|
|
||||||
const { t } = useTranslation('settings/customization/opacity-selector');
|
const { t } = useTranslation('settings/customization/opacity-selector');
|
||||||
|
|
||||||
const { name: configName } = useConfigContext();
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
|
||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|||||||
@@ -15,17 +15,13 @@ import { useConfigContext } from '../../../../config/provider';
|
|||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { useColorTheme } from '../../../../tools/color';
|
import { useColorTheme } from '../../../../tools/color';
|
||||||
|
|
||||||
interface ShadeSelectorProps {
|
export function ShadeSelector() {
|
||||||
defaultValue: MantineTheme['primaryShade'] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShadeSelector({ defaultValue }: ShadeSelectorProps) {
|
|
||||||
const { t } = useTranslation('settings/customization/shade-selector');
|
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 [popoverOpened, popover] = useDisclosure(false);
|
||||||
const { primaryColor, setPrimaryShade } = useColorTheme();
|
const { primaryColor, setPrimaryShade } = useColorTheme();
|
||||||
|
|
||||||
const { name: configName } = useConfigContext();
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ import { useCardStyles } from '../../../useCardStyles';
|
|||||||
export const ToggleEditModeAction = () => {
|
export const ToggleEditModeAction = () => {
|
||||||
const { enabled, toggleEditMode } = useEditModeStore();
|
const { enabled, toggleEditMode } = useEditModeStore();
|
||||||
const namedWrapperColumnCount = useNamedWrapperColumnCount();
|
const namedWrapperColumnCount = useNamedWrapperColumnCount();
|
||||||
const { t } = useTranslation('layout/header/actions/toggle-edit-mode');
|
const { t } = useTranslation(['layout/header/actions/toggle-edit-mode', 'common']);
|
||||||
const translatedSize = t(`screenSizes.${namedWrapperColumnCount}`);
|
const translatedSize =
|
||||||
|
namedWrapperColumnCount !== null
|
||||||
|
? t(`common:breakPoints.${namedWrapperColumnCount}`)
|
||||||
|
: t('common:loading');
|
||||||
|
|
||||||
const smallerThanSm = useScreenSmallerThan('sm');
|
const smallerThanSm = useScreenSmallerThan('sm');
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
|
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { REPO_URL } from '../../../../data/constants';
|
import { REPO_URL } from '../../../../data/constants';
|
||||||
import DockerMenuButton from '../../../modules/Docker/DockerModule';
|
import DockerMenuButton from '../../../modules/Docker/DockerModule';
|
||||||
import { usePackageAttributesStore } from '../../../tools/client/zustands/usePackageAttributesStore';
|
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 { WidgetsRemoveModal } from '../components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
|
||||||
import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal';
|
import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal';
|
||||||
import { ConfigProvider } from '../config/provider';
|
import { ConfigProvider } from '../config/provider';
|
||||||
import '../styles/global.scss';
|
|
||||||
import { ColorTheme } from '../tools/color';
|
import { ColorTheme } from '../tools/color';
|
||||||
import { queryClient } from '../tools/queryClient';
|
import { queryClient } from '../tools/queryClient';
|
||||||
import { theme } from '../tools/theme';
|
import { theme } from '../tools/theme';
|
||||||
@@ -28,6 +27,9 @@ import {
|
|||||||
} from '../tools/server/getPackageVersion';
|
} from '../tools/server/getPackageVersion';
|
||||||
import { usePackageAttributesStore } from '../tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '../tools/client/zustands/usePackageAttributesStore';
|
||||||
|
|
||||||
|
import '../styles/global.scss';
|
||||||
|
import '@uiw/react-textarea-code-editor/dist.css';
|
||||||
|
|
||||||
function App(
|
function App(
|
||||||
this: any,
|
this: any,
|
||||||
props: AppProps & { colorScheme: ColorScheme; packageAttributes: ServerSidePackageAttributesType }
|
props: AppProps & { colorScheme: ColorScheme; packageAttributes: ServerSidePackageAttributesType }
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Styling for grid-stack main area
|
// 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-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-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}) }
|
.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)}) }
|
.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}) }
|
.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 }
|
.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 }
|
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-x="#{$i}"] { left: 128px * $i }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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/internationalization',
|
||||||
'settings/general/search-engine',
|
'settings/general/search-engine',
|
||||||
'settings/general/widget-positions',
|
'settings/general/widget-positions',
|
||||||
|
'settings/customization/general',
|
||||||
'settings/customization/color-selector',
|
'settings/customization/color-selector',
|
||||||
'settings/customization/page-appearance',
|
'settings/customization/page-appearance',
|
||||||
'settings/customization/shade-selector',
|
'settings/customization/shade-selector',
|
||||||
'settings/customization/app-width',
|
'settings/customization/app-width',
|
||||||
'settings/customization/opacity-selector',
|
'settings/customization/opacity-selector',
|
||||||
|
'settings/customization/gridstack',
|
||||||
'modules/common',
|
'modules/common',
|
||||||
'modules/date',
|
'modules/date',
|
||||||
'modules/calendar',
|
'modules/calendar',
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ export interface CustomizationSettingsType {
|
|||||||
customCss?: string;
|
customCss?: string;
|
||||||
colors: ColorsCustomizationSettingsType;
|
colors: ColorsCustomizationSettingsType;
|
||||||
appOpacity?: number;
|
appOpacity?: number;
|
||||||
|
gridstack?: GridstackSettingsType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridstackSettingsType {
|
||||||
|
columnCountSmall: number; // default: 3
|
||||||
|
columnCountMedium: number; // default: 6
|
||||||
|
columnCountLarge: number; // default: 12
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LayoutCustomizationSettingsType {
|
interface LayoutCustomizationSettingsType {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Group,
|
Group,
|
||||||
Indicator,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
@@ -41,7 +40,7 @@ const definition = defineWidget({
|
|||||||
component: TorrentNetworkTrafficTile,
|
component: TorrentNetworkTrafficTile,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ITorrentNetworkTraffic = IWidget<typeof definition['id'], typeof definition>;
|
export type ITorrentNetworkTraffic = IWidget<(typeof definition)['id'], typeof definition>;
|
||||||
|
|
||||||
interface TorrentNetworkTrafficTileProps {
|
interface TorrentNetworkTrafficTileProps {
|
||||||
widget: ITorrentNetworkTraffic;
|
widget: ITorrentNetworkTraffic;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TorrentState } from '@ctrl/shared-torrent';
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Center,
|
Center,
|
||||||
@@ -52,7 +51,7 @@ const definition = defineWidget({
|
|||||||
component: TorrentTile,
|
component: TorrentTile,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ITorrent = IWidget<typeof definition['id'], typeof definition>;
|
export type ITorrent = IWidget<(typeof definition)['id'], typeof definition>;
|
||||||
|
|
||||||
interface TorrentTileProps {
|
interface TorrentTileProps {
|
||||||
widget: ITorrent;
|
widget: ITorrent;
|
||||||
|
|||||||
Reference in New Issue
Block a user