Merge branch 'dev' of https://github.com/ajnart/homarr into dev

This commit is contained in:
Manuel Ruwe
2023-01-14 21:15:24 +01:00
22 changed files with 89 additions and 70 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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": {

View File

@@ -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..."
} }
} }
} }

View File

@@ -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)`}

View File

@@ -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}

View File

@@ -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,
}, },

View File

@@ -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')}

View File

@@ -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>

View File

@@ -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>
); );
}; };

View File

@@ -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: {

View File

@@ -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:

View File

@@ -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={{

View File

@@ -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: {

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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,
}; };

View File

@@ -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();

View File

@@ -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();

View File

@@ -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