mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-13 17:05:47 +01:00
Merge branch 'dev' of https://github.com/ajnart/homarr into dev
This commit is contained in:
@@ -369,7 +369,7 @@
|
|||||||
},
|
},
|
||||||
"pageTitle": "Homarr v0.11 ⭐️",
|
"pageTitle": "Homarr v0.11 ⭐️",
|
||||||
"logoImageUrl": "/imgs/logo/logo.png",
|
"logoImageUrl": "/imgs/logo/logo.png",
|
||||||
"faviconUrl": "/imgs/logo/logo.png",
|
"faviconUrl": "/imgs/favicon/favicon-squared",
|
||||||
"backgroundImageUrl": "",
|
"backgroundImageUrl": "",
|
||||||
"customCss": "",
|
"customCss": "",
|
||||||
"colors": {
|
"colors": {
|
||||||
@@ -380,4 +380,4 @@
|
|||||||
"appOpacity": 100
|
"appOpacity": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -9,6 +9,7 @@
|
|||||||
"version": "Version",
|
"version": "Version",
|
||||||
"changePosition": "Change position",
|
"changePosition": "Change position",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
|
"removeConfirm": "Are you sure that you want to remove {{item}} ?",
|
||||||
"sections": {
|
"sections": {
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"dangerZone": "Danger zone"
|
"dangerZone": "Danger zone"
|
||||||
|
|||||||
@@ -19,10 +19,6 @@
|
|||||||
"url": {
|
"url": {
|
||||||
"label": "Dash. URL"
|
"label": "Dash. URL"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"remove": {
|
|
||||||
"title": "Remove Dash. widget",
|
|
||||||
"confirm": "Are you sure, that you want to remove the Dash. widget?"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"descriptor": {
|
"descriptor": {
|
||||||
"name": "BitTorrent",
|
"name": "Torrent",
|
||||||
"description": "Displays a list of the torrent which are currently downloading",
|
"description": "Displays a list of the torrent which are currently downloading",
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings for BitTorrent integration",
|
"title": "Settings for Torrent integration",
|
||||||
"refreshInterval": {
|
"refreshInterval": {
|
||||||
"label": "Refresh interval (in seconds)"
|
"label": "Refresh interval (in seconds)"
|
||||||
},
|
},
|
||||||
@@ -51,4 +51,4 @@
|
|||||||
"title": "Loading..."
|
"title": "Loading..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
<GenericSecretInput
|
<GenericSecretInput
|
||||||
onClickUpdateButton={(value) => {
|
onClickUpdateButton={(value) => {
|
||||||
form.setFieldValue(`integration.properties.${index}.value`, value);
|
form.setFieldValue(`integration.properties.${index}.value`, value);
|
||||||
form.setFieldValue(`integration.properties.${index}.isDefined`, value !== undefined);
|
form.setFieldValue(
|
||||||
|
`integration.properties.${index}.isDefined`,
|
||||||
|
value !== undefined
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
key={`input-${property}`}
|
key={`input-${property}`}
|
||||||
label={`${property} (potentionally unmapped)`}
|
label={`${property} (potentionally unmapped)`}
|
||||||
|
|||||||
@@ -58,14 +58,6 @@ export const WidgetsEditModal = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMutliselectData = (option: string) => {
|
|
||||||
const currentWidgetDefinition = Widgets[innerProps.widgetId as keyof typeof Widgets];
|
|
||||||
if (!Widgets) return [];
|
|
||||||
|
|
||||||
const options = currentWidgetDefinition.options as any;
|
|
||||||
return options[option]?.data ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
updateConfig(
|
updateConfig(
|
||||||
configName,
|
configName,
|
||||||
@@ -85,9 +77,9 @@ export const WidgetsEditModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
{items.map(([key, defaultValue], index) => {
|
{items.map(([key, _], index) => {
|
||||||
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
|
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
|
||||||
const value = moduleProperties[key] ?? defaultValue;
|
const value = moduleProperties[key] ?? option.defaultValue;
|
||||||
|
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return (
|
return (
|
||||||
@@ -183,10 +175,10 @@ function WidgetOptionTypeSwitch(
|
|||||||
case 'slider':
|
case 'slider':
|
||||||
return (
|
return (
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
|
||||||
<Slider
|
<Slider
|
||||||
color={primaryColor}
|
color={primaryColor}
|
||||||
key={`${option.type}-${index}`}
|
key={`${option.type}-${index}`}
|
||||||
|
label={value}
|
||||||
value={value as number}
|
value={value as number}
|
||||||
min={option.min}
|
min={option.min}
|
||||||
max={option.max}
|
max={option.max}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
|||||||
const handleDeleteClick = () => {
|
const handleDeleteClick = () => {
|
||||||
openContextModalGeneric<WidgetsRemoveModalInnerProps>({
|
openContextModalGeneric<WidgetsRemoveModalInnerProps>({
|
||||||
modal: 'integrationRemove',
|
modal: 'integrationRemove',
|
||||||
title: <Title order={4}>{t('descriptor.remove.title')}</Title>,
|
title: <Title order={4}>{t('common:remove')}</Title>,
|
||||||
innerProps: {
|
innerProps: {
|
||||||
widgetId: integration,
|
widgetId: integration,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Group, Stack, Text } from '@mantine/core';
|
import { Button, Group, Stack, Text } from '@mantine/core';
|
||||||
import { ContextModalProps } from '@mantine/modals';
|
import { ContextModalProps } from '@mantine/modals';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
@@ -32,7 +32,11 @@ export const WidgetsRemoveModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>{t('descriptor.remove.confirm')}</Text>
|
<Trans
|
||||||
|
i18nKey="common:removeConfirm"
|
||||||
|
components={[<Text weight={500} />]}
|
||||||
|
values={{ item: innerProps.widgetId }}
|
||||||
|
/>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button onClick={() => context.closeModal(id)} variant="light">
|
<Button onClick={() => context.closeModal(id)} variant="light">
|
||||||
{t('common:cancel')}
|
{t('common:cancel')}
|
||||||
|
|||||||
@@ -27,7 +27,12 @@ export const DashboardEditView = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<ActionIcon size="sm">
|
<ActionIcon
|
||||||
|
size="sm"
|
||||||
|
component="a"
|
||||||
|
href="https://homarr.dev/docs/customizations/layout"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<IconQuestionMark size={16} />
|
<IconQuestionMark size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ interface DashboardSidebarProps extends DashboardSidebarInnerProps {
|
|||||||
isGridstackReady: boolean;
|
isGridstackReady: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardSidebar = ({ location, isGridstackReady }: DashboardSidebarProps) => (
|
export const DashboardSidebar = ({ location, isGridstackReady }: DashboardSidebarProps) => {
|
||||||
<Card
|
const {
|
||||||
withBorder
|
cx,
|
||||||
w={300}
|
classes: { card: cardClass },
|
||||||
style={{
|
} = useCardStyles(false);
|
||||||
background: 'none',
|
|
||||||
borderStyle: 'dashed',
|
return (
|
||||||
}}
|
<Card p={0} m={0} radius="lg" className={cardClass} w={300}>
|
||||||
>
|
{isGridstackReady && <SidebarInner location={location} />}
|
||||||
{isGridstackReady && <SidebarInner location={location} />}
|
</Card>
|
||||||
</Card>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
interface DashboardSidebarInnerProps {
|
interface DashboardSidebarInnerProps {
|
||||||
location: 'right' | 'left';
|
location: 'right' | 'left';
|
||||||
@@ -37,17 +37,18 @@ const SidebarInner = ({ location }: DashboardSidebarInnerProps) => {
|
|||||||
} = useCardStyles(false);
|
} = useCardStyles(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card withBorder mih="100%" p={0} radius="lg" className={cardClass} ref={refs.wrapper}>
|
<div
|
||||||
<div
|
ref={refs.wrapper}
|
||||||
className="grid-stack grid-stack-sidebar"
|
className="grid-stack grid-stack-sidebar"
|
||||||
style={{ transitionDuration: '0s', height: '100%' }}
|
style={{
|
||||||
data-sidebar={location}
|
transitionDuration: '0s',
|
||||||
// eslint-disable-next-line react/no-unknown-property
|
}}
|
||||||
gs-min-row={minRow}
|
data-sidebar={location}
|
||||||
>
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
|
gs-min-row={minRow}
|
||||||
</div>
|
>
|
||||||
</Card>
|
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const initializeGridstack = (
|
|||||||
items: AppType[],
|
items: AppType[],
|
||||||
widgets: IWidget<string, any>[],
|
widgets: IWidget<string, any>[],
|
||||||
isEditMode: boolean,
|
isEditMode: boolean,
|
||||||
wrapperColumnCount: 3 | 6 | 12,
|
wrapperColumnCount: number,
|
||||||
shapeSize: 'sm' | 'md' | 'lg',
|
shapeSize: 'sm' | 'md' | 'lg',
|
||||||
tilesWithUnknownLocation: TileWithUnknownLocation[],
|
tilesWithUnknownLocation: TileWithUnknownLocation[],
|
||||||
events: {
|
events: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMantineTheme } from '@mantine/core';
|
import { useMantineTheme } from '@mantine/core';
|
||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
|
||||||
export const useGridstackStore = create<GridstackStoreType>((set, get) => ({
|
export const useGridstackStore = create<GridstackStoreType>((set, get) => ({
|
||||||
mainAreaWidth: null,
|
mainAreaWidth: null,
|
||||||
@@ -27,11 +28,16 @@ export const useNamedWrapperColumnCount = (): 'small' | 'medium' | 'large' | nul
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useWrapperColumnCount = () => {
|
export const useWrapperColumnCount = () => {
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
const numberOfSidebars =
|
||||||
|
(config?.settings.customization.layout.enabledLeftSidebar ? 1 : 0) +
|
||||||
|
(config?.settings.customization.layout.enabledRightSidebar ? 1 : 0);
|
||||||
|
|
||||||
switch (useNamedWrapperColumnCount()) {
|
switch (useNamedWrapperColumnCount()) {
|
||||||
case 'large':
|
case 'large':
|
||||||
return 12;
|
return 15 - numberOfSidebars * 2;
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return 6;
|
return 9 - numberOfSidebars * 2;
|
||||||
case 'small':
|
case 'small':
|
||||||
return 3;
|
return 3;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -77,8 +77,14 @@ export default function LanguageSelect() {
|
|||||||
filter={(value, item) => {
|
filter={(value, item) => {
|
||||||
const selectItems = item as unknown as { value: string; language: Language };
|
const selectItems = item as unknown as { value: string; language: Language };
|
||||||
return (
|
return (
|
||||||
selectItems.language.originalName.trim().includes(value.trim()) ||
|
selectItems.language.originalName
|
||||||
selectItems.language.translatedName.trim().includes(value.trim())
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.includes(value.toLowerCase().trim()) ||
|
||||||
|
selectItems.language.translatedName
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.includes(value.toLowerCase().trim())
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { setCookie } from 'cookies-next';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { GetServerSidePropsContext } from 'next';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -38,6 +39,12 @@ export async function getServerSideProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = getFrontendConfig(configName as string);
|
const config = getFrontendConfig(configName as string);
|
||||||
|
setCookie('config-name', configName, {
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
|
sameSite: 'strict',
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function Put(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const targetPath = path.join('data/configs', `${slug}.json`);
|
const targetPath = path.join('data/configs', `${slug}.json`);
|
||||||
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||||
|
|
||||||
Consola.info(`Config '${slug}' has been updated and flushed to '${targetPath}'.`);
|
Consola.debug(`Config '${slug}' has been updated and flushed to '${targetPath}'.`);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
message: 'Configuration saved with success',
|
message: 'Configuration saved with success',
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ export async function getServerSideProps({
|
|||||||
|
|
||||||
const translations = await getServerSideTranslations(req, res, dashboardNamespaces, locale);
|
const translations = await getServerSideTranslations(req, res, dashboardNamespaces, locale);
|
||||||
|
|
||||||
Consola.info(`Decided to use configuration '${configName}'`);
|
|
||||||
|
|
||||||
const config = getFrontendConfig(configName as string);
|
const config = getFrontendConfig(configName as string);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AreaType } from '../../types/area';
|
|||||||
import { CategoryType } from '../../types/category';
|
import { CategoryType } from '../../types/category';
|
||||||
import { ConfigType } from '../../types/config';
|
import { ConfigType } from '../../types/config';
|
||||||
import { SearchEngineCommonSettingsType } from '../../types/settings';
|
import { SearchEngineCommonSettingsType } from '../../types/settings';
|
||||||
import { IBitTorrent } from '../../widgets/bitTorrent/BitTorrentTile';
|
import { ITorrent } from '../../widgets/torrent/TorrentTile';
|
||||||
import { ICalendarWidget } from '../../widgets/calendar/CalendarTile';
|
import { ICalendarWidget } from '../../widgets/calendar/CalendarTile';
|
||||||
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
|
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
|
||||||
import { IDateWidget } from '../../widgets/date/DateTile';
|
import { IDateWidget } from '../../widgets/date/DateTile';
|
||||||
@@ -194,7 +194,7 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
shape: {},
|
shape: {},
|
||||||
} as IBitTorrent;
|
} as ITorrent;
|
||||||
case 'weather':
|
case 'weather':
|
||||||
return {
|
return {
|
||||||
id: 'weather',
|
id: 'weather',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import calendar from './calendar/CalendarTile';
|
|||||||
import dashdot from './dashDot/DashDotTile';
|
import dashdot from './dashDot/DashDotTile';
|
||||||
import usenet from './useNet/UseNetTile';
|
import usenet from './useNet/UseNetTile';
|
||||||
import weather from './weather/WeatherTile';
|
import weather from './weather/WeatherTile';
|
||||||
import bitTorrent from './bitTorrent/BitTorrentTile';
|
import torrent from './torrent/TorrentTile';
|
||||||
import torrentNetworkTraffic from './torrentNetworkTraffic/TorrentNetworkTrafficTile';
|
import torrentNetworkTraffic from './torrentNetworkTraffic/TorrentNetworkTrafficTile';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
dashdot,
|
dashdot,
|
||||||
usenet,
|
usenet,
|
||||||
weather,
|
weather,
|
||||||
'torrents-status': bitTorrent,
|
'torrents-status': torrent,
|
||||||
dlspeed: torrentNetworkTraffic,
|
dlspeed: torrentNetworkTraffic,
|
||||||
date,
|
date,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { useElementSize } from '@mantine/hooks';
|
|||||||
import { calculateETA } from '../../tools/calculateEta';
|
import { calculateETA } from '../../tools/calculateEta';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
|
|
||||||
interface BitTorrentQueueItemProps {
|
interface TorrentQueueItemProps {
|
||||||
torrent: NormalizedTorrent;
|
torrent: NormalizedTorrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BitTorrrentQueueItem = ({ torrent }: BitTorrentQueueItemProps) => {
|
export const BitTorrrentQueueItem = ({ torrent }: TorrentQueueItemProps) => {
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
||||||
const { width } = useElementSize();
|
const { width } = useElementSize();
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ import { useGetTorrentData } from '../../hooks/widgets/torrents/useGetTorrentDat
|
|||||||
import { AppIntegrationType } from '../../types/app';
|
import { AppIntegrationType } from '../../types/app';
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { BitTorrrentQueueItem } from './BitTorrentQueueItem';
|
import { BitTorrrentQueueItem } from './TorrentQueueItem';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -56,16 +56,16 @@ const definition = defineWidget({
|
|||||||
maxWidth: 12,
|
maxWidth: 12,
|
||||||
maxHeight: 14,
|
maxHeight: 14,
|
||||||
},
|
},
|
||||||
component: BitTorrentTile,
|
component: TorrentTile,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type IBitTorrent = IWidget<typeof definition['id'], typeof definition>;
|
export type ITorrent = IWidget<typeof definition['id'], typeof definition>;
|
||||||
|
|
||||||
interface BitTorrentTileProps {
|
interface TorrentTileProps {
|
||||||
widget: IBitTorrent;
|
widget: ITorrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BitTorrentTile({ widget }: BitTorrentTileProps) {
|
function TorrentTile({ widget }: TorrentTileProps) {
|
||||||
const { t } = useTranslation('modules/torrents-status');
|
const { t } = useTranslation('modules/torrents-status');
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
||||||
const { width } = useElementSize();
|
const { width } = useElementSize();
|
||||||
@@ -39,8 +39,8 @@ export type IWidgetOptionValue =
|
|||||||
|
|
||||||
// Interface for data type
|
// Interface for data type
|
||||||
interface DataType {
|
interface DataType {
|
||||||
label: string
|
label: string;
|
||||||
value: string
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// will show a multi-select with specified data
|
// will show a multi-select with specified data
|
||||||
|
|||||||
Reference in New Issue
Block a user