mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
🔥 Remove old and unused components
This commit is contained in:
@@ -28,5 +28,6 @@ module.exports = {
|
|||||||
'@typescript-eslint/no-shadow': 'off',
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
'@typescript-eslint/no-use-before-define': 'off',
|
'@typescript-eslint/no-use-before-define': 'off',
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'linebreak-style': 0
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Group, Text, useMantineTheme } from '@mantine/core';
|
import { Group, Text, useMantineTheme } from '@mantine/core';
|
||||||
import { IconX as X, IconCheck as Check, IconX, IconPhoto, IconUpload } from '@tabler/icons';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { setCookie } from 'cookies-next';
|
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { showNotification } from '@mantine/notifications';
|
||||||
|
import { IconCheck as Check, IconPhoto, IconUpload, IconX, IconX as X } from '@tabler/icons';
|
||||||
|
import { setCookie } from 'cookies-next';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfigStore } from '../../config/store';
|
||||||
import { Config } from '../../tools/types';
|
import { Config } from '../../tools/types';
|
||||||
import { migrateToIdConfig } from '../../tools/migrate';
|
|
||||||
|
|
||||||
export default function LoadConfigComponent(props: any) {
|
export default function LoadConfigComponent(props: any) {
|
||||||
const { setConfig } = useConfig();
|
const { updateConfig } = useConfigStore();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const { t } = useTranslation('settings/general/config-changer');
|
const { t } = useTranslation('settings/general/config-changer');
|
||||||
|
|
||||||
@@ -48,8 +47,7 @@ export default function LoadConfigComponent(props: any) {
|
|||||||
maxAge: 60 * 60 * 24 * 30,
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
});
|
});
|
||||||
const migratedConfig = migrateToIdConfig(newConfig);
|
updateConfig(newConfig.name, (previousConfig) => ({ ...previousConfig, newConfig }));
|
||||||
setConfig(migratedConfig);
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
accept={['application/json']}
|
accept={['application/json']}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react/no-unknown-property */
|
||||||
import { ReactNode, RefObject } from 'react';
|
import { ReactNode, RefObject } from 'react';
|
||||||
|
|
||||||
interface GridstackTileWrapperProps {
|
interface GridstackTileWrapperProps {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { useConfigContext } from '../../../../config/provider';
|
|||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { CategoryType } from '../../../../types/category';
|
import { CategoryType } from '../../../../types/category';
|
||||||
|
|
||||||
export interface CategoryEditModalInnerProps {
|
export type CategoryEditModalInnerProps = {
|
||||||
category: CategoryType;
|
category: CategoryType;
|
||||||
onSuccess: (category: CategoryType) => Promise<void>;
|
onSuccess: (category: CategoryType) => Promise<void>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const CategoryEditModal = ({
|
export const CategoryEditModal = ({
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const DashboardSidebar = ({ location }: DashboardSidebarProps) => {
|
|||||||
className="grid-stack grid-stack-sidebar"
|
className="grid-stack grid-stack-sidebar"
|
||||||
style={{ transitionDuration: '0s', height: '100%' }}
|
style={{ transitionDuration: '0s', height: '100%' }}
|
||||||
data-sidebar={location}
|
data-sidebar={location}
|
||||||
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
gs-min-row={minRow}
|
gs-min-row={minRow}
|
||||||
ref={refs.wrapper}
|
ref={refs.wrapper}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Aside as MantineAside, createStyles } from '@mantine/core';
|
|
||||||
import Widgets from './Widgets';
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
|
||||||
hide: {
|
|
||||||
[theme.fn.smallerThan('xs')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
burger: {
|
|
||||||
[theme.fn.largerThan('sm')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function Aside(props: any) {
|
|
||||||
const { classes, cx } = useStyles();
|
|
||||||
return (
|
|
||||||
<MantineAside
|
|
||||||
pr="md"
|
|
||||||
hiddenBreakpoint="sm"
|
|
||||||
hidden
|
|
||||||
className={cx(classes.hide)}
|
|
||||||
style={{
|
|
||||||
border: 'none',
|
|
||||||
background: 'none',
|
|
||||||
}}
|
|
||||||
width={{
|
|
||||||
base: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Widgets />
|
|
||||||
</MantineAside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { createStyles, Navbar as MantineNavbar } from '@mantine/core';
|
|
||||||
import Widgets from './Widgets';
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
|
||||||
hide: {
|
|
||||||
[theme.fn.smallerThan('xs')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
burger: {
|
|
||||||
[theme.fn.largerThan('sm')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function Navbar() {
|
|
||||||
const { classes, cx } = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MantineNavbar
|
|
||||||
pl="md"
|
|
||||||
hiddenBreakpoint="sm"
|
|
||||||
hidden
|
|
||||||
className={cx(classes.hide)}
|
|
||||||
style={{
|
|
||||||
border: 'none',
|
|
||||||
background: 'none',
|
|
||||||
}}
|
|
||||||
width={{
|
|
||||||
base: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Widgets />
|
|
||||||
</MantineNavbar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Stack } from '@mantine/core';
|
|
||||||
import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../../modules';
|
|
||||||
import { DashdotModule } from '../../modules/dashdot';
|
|
||||||
import { ModuleWrapper } from '../../modules/moduleWrapper';
|
|
||||||
|
|
||||||
export default function Widgets(props: any) {
|
|
||||||
return (
|
|
||||||
<Stack my={16} style={{ width: 300 }}>
|
|
||||||
<ModuleWrapper module={CalendarModule} />
|
|
||||||
<ModuleWrapper module={TotalDownloadsModule} />
|
|
||||||
<ModuleWrapper module={WeatherModule} />
|
|
||||||
<ModuleWrapper module={DateModule} />
|
|
||||||
<ModuleWrapper module={DashdotModule} />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -58,7 +58,9 @@ export function Search() {
|
|||||||
// TODO: ask manuel-rw about overseerr
|
// TODO: ask manuel-rw about overseerr
|
||||||
// Answer: We can simply check if there is a app of the type overseer and display results if there is one.
|
// Answer: We can simply check if there is a app of the type overseer and display results if there is one.
|
||||||
// Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results
|
// Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results
|
||||||
const isOverseerrEnabled = false; //config?.settings.common.enabledModules.overseerr;
|
const isOverseerrEnabled = config?.apps.some(
|
||||||
|
(x) => x.integration.type === 'overseerr' || x.integration.type === 'jellyseerr'
|
||||||
|
);
|
||||||
const overseerrApp = config?.apps.find(
|
const overseerrApp = config?.apps.find(
|
||||||
(app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
|
(app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
/* eslint-disable react/no-children-prop */
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Divider,
|
|
||||||
Indicator,
|
|
||||||
Popover,
|
|
||||||
ScrollArea,
|
|
||||||
createStyles,
|
|
||||||
useMantineTheme,
|
|
||||||
Space,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Calendar } from '@mantine/dates';
|
|
||||||
import { IconCalendar as CalendarIcon } from '@tabler/icons';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { IModule } from '../ModuleTypes';
|
|
||||||
import {
|
|
||||||
SonarrMediaDisplay,
|
|
||||||
RadarrMediaDisplay,
|
|
||||||
LidarrMediaDisplay,
|
|
||||||
ReadarrMediaDisplay,
|
|
||||||
} from '../common';
|
|
||||||
import { serviceItem } from '../../tools/types';
|
|
||||||
import { useColorTheme } from '../../tools/color';
|
|
||||||
|
|
||||||
export const CalendarModule: IModule = {
|
|
||||||
title: 'Calendar',
|
|
||||||
icon: CalendarIcon,
|
|
||||||
component: CalendarComponent,
|
|
||||||
options: {
|
|
||||||
sundaystart: {
|
|
||||||
name: 'descriptor.settings.sundayStart.label',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'calendar',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CalendarComponent(props: any) {
|
|
||||||
const { config } = useConfig();
|
|
||||||
const theme = useMantineTheme();
|
|
||||||
const { secondaryColor } = useColorTheme();
|
|
||||||
const useStyles = createStyles((theme) => ({
|
|
||||||
weekend: {
|
|
||||||
color: `${secondaryColor} !important`,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const [sonarrMedias, setSonarrMedias] = useState([] as any);
|
|
||||||
const [lidarrMedias, setLidarrMedias] = useState([] as any);
|
|
||||||
const [radarrMedias, setRadarrMedias] = useState([] as any);
|
|
||||||
const [readarrMedias, setReadarrMedias] = useState([] as any);
|
|
||||||
const sonarrServices = config.apps.filter((service) => service.type === 'Sonarr');
|
|
||||||
const radarrServices = config.apps.filter((service) => service.type === 'Radarr');
|
|
||||||
const lidarrServices = config.apps.filter((service) => service.type === 'Lidarr');
|
|
||||||
const readarrServices = config.apps.filter((service) => service.type === 'Readarr');
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
const { classes, cx } = useStyles();
|
|
||||||
|
|
||||||
function getMedias(service: serviceItem | undefined, type: string) {
|
|
||||||
if (!service || !service.apiKey) {
|
|
||||||
return Promise.resolve({ data: [] });
|
|
||||||
}
|
|
||||||
return axios.post(`/api/modules/calendar?type=${type}`, { id: service.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Create each Sonarr service and get the medias
|
|
||||||
const currentSonarrMedias: any[] = [];
|
|
||||||
Promise.all(
|
|
||||||
sonarrServices.map((service) =>
|
|
||||||
getMedias(service, 'sonarr')
|
|
||||||
.then((res) => {
|
|
||||||
currentSonarrMedias.push(...res.data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
currentSonarrMedias.push([]);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).then(() => {
|
|
||||||
setSonarrMedias(currentSonarrMedias);
|
|
||||||
});
|
|
||||||
const currentRadarrMedias: any[] = [];
|
|
||||||
Promise.all(
|
|
||||||
radarrServices.map((service) =>
|
|
||||||
getMedias(service, 'radarr')
|
|
||||||
.then((res) => {
|
|
||||||
currentRadarrMedias.push(...res.data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
currentRadarrMedias.push([]);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).then(() => {
|
|
||||||
setRadarrMedias(currentRadarrMedias);
|
|
||||||
});
|
|
||||||
const currentLidarrMedias: any[] = [];
|
|
||||||
Promise.all(
|
|
||||||
lidarrServices.map((service) =>
|
|
||||||
getMedias(service, 'lidarr')
|
|
||||||
.then((res) => {
|
|
||||||
currentLidarrMedias.push(...res.data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
currentLidarrMedias.push([]);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).then(() => {
|
|
||||||
setLidarrMedias(currentLidarrMedias);
|
|
||||||
});
|
|
||||||
const currentReadarrMedias: any[] = [];
|
|
||||||
Promise.all(
|
|
||||||
readarrServices.map((service) =>
|
|
||||||
getMedias(service, 'readarr')
|
|
||||||
.then((res) => {
|
|
||||||
currentReadarrMedias.push(...res.data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
currentReadarrMedias.push([]);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).then(() => {
|
|
||||||
setReadarrMedias(currentReadarrMedias);
|
|
||||||
});
|
|
||||||
}, [config.apps]);
|
|
||||||
|
|
||||||
const weekStartsAtSunday =
|
|
||||||
(config?.modules?.[CalendarModule.id]?.options?.sundaystart?.value as boolean) ?? false;
|
|
||||||
return (
|
|
||||||
<Calendar
|
|
||||||
firstDayOfWeek={weekStartsAtSunday ? 'sunday' : 'monday'}
|
|
||||||
onChange={(day: any) => {}}
|
|
||||||
dayStyle={(date) =>
|
|
||||||
date.getDay() === today.getDay() && date.getDate() === today.getDate()
|
|
||||||
? {
|
|
||||||
backgroundColor:
|
|
||||||
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[0],
|
|
||||||
margin: 1,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
margin: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allowLevelChange={false}
|
|
||||||
dayClassName={(date, modifiers) => cx({ [classes.weekend]: modifiers.weekend })}
|
|
||||||
renderDay={(renderdate) => (
|
|
||||||
<DayComponent
|
|
||||||
renderdate={renderdate}
|
|
||||||
sonarrmedias={sonarrMedias}
|
|
||||||
radarrmedias={radarrMedias}
|
|
||||||
lidarrmedias={lidarrMedias}
|
|
||||||
readarrmedias={readarrMedias}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DayComponent(props: any) {
|
|
||||||
const {
|
|
||||||
renderdate,
|
|
||||||
sonarrmedias,
|
|
||||||
radarrmedias,
|
|
||||||
lidarrmedias,
|
|
||||||
readarrmedias,
|
|
||||||
}: { renderdate: Date; sonarrmedias: []; radarrmedias: []; lidarrmedias: []; readarrmedias: [] } =
|
|
||||||
props;
|
|
||||||
const [opened, { close, open }] = useDisclosure(false);
|
|
||||||
|
|
||||||
const day = renderdate.getDate();
|
|
||||||
|
|
||||||
const readarrFiltered = readarrmedias.filter((media: any) => {
|
|
||||||
const date = new Date(media.releaseDate);
|
|
||||||
return date.toDateString() === renderdate.toDateString();
|
|
||||||
});
|
|
||||||
|
|
||||||
const lidarrFiltered = lidarrmedias.filter((media: any) => {
|
|
||||||
const date = new Date(media.releaseDate);
|
|
||||||
return date.toDateString() === renderdate.toDateString();
|
|
||||||
});
|
|
||||||
const sonarrFiltered = sonarrmedias.filter((media: any) => {
|
|
||||||
const date = new Date(media.airDateUtc);
|
|
||||||
return date.toDateString() === renderdate.toDateString();
|
|
||||||
});
|
|
||||||
const radarrFiltered = radarrmedias.filter((media: any) => {
|
|
||||||
const date = new Date(media.inCinemas);
|
|
||||||
return date.toDateString() === renderdate.toDateString();
|
|
||||||
});
|
|
||||||
const totalFiltered = [
|
|
||||||
...readarrFiltered,
|
|
||||||
...lidarrFiltered,
|
|
||||||
...sonarrFiltered,
|
|
||||||
...radarrFiltered,
|
|
||||||
];
|
|
||||||
if (totalFiltered.length === 0) {
|
|
||||||
return <div>{day}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
position="bottom"
|
|
||||||
withArrow
|
|
||||||
withinPortal
|
|
||||||
radius="lg"
|
|
||||||
shadow="sm"
|
|
||||||
transition="pop"
|
|
||||||
onClose={close}
|
|
||||||
opened={opened}
|
|
||||||
>
|
|
||||||
<Popover.Target>
|
|
||||||
<Box onClick={open}>
|
|
||||||
{readarrFiltered.length > 0 && (
|
|
||||||
<Indicator
|
|
||||||
size={10}
|
|
||||||
withBorder
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 8,
|
|
||||||
left: 8,
|
|
||||||
}}
|
|
||||||
color="red"
|
|
||||||
children={null}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{radarrFiltered.length > 0 && (
|
|
||||||
<Indicator
|
|
||||||
size={10}
|
|
||||||
withBorder
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 8,
|
|
||||||
left: 8,
|
|
||||||
}}
|
|
||||||
color="yellow"
|
|
||||||
children={null}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{sonarrFiltered.length > 0 && (
|
|
||||||
<Indicator
|
|
||||||
size={10}
|
|
||||||
withBorder
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
}}
|
|
||||||
color="blue"
|
|
||||||
children={null}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{lidarrFiltered.length > 0 && (
|
|
||||||
<Indicator
|
|
||||||
size={10}
|
|
||||||
withBorder
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 8,
|
|
||||||
right: 8,
|
|
||||||
}}
|
|
||||||
color="green"
|
|
||||||
children={undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div>{day}</div>
|
|
||||||
</Box>
|
|
||||||
</Popover.Target>
|
|
||||||
<Popover.Dropdown>
|
|
||||||
<ScrollArea
|
|
||||||
offsetScrollbars
|
|
||||||
scrollbarSize={5}
|
|
||||||
style={{
|
|
||||||
height:
|
|
||||||
totalFiltered.slice(0, 2).length > 1 ? totalFiltered.slice(0, 2).length * 150 : 220,
|
|
||||||
width: 400,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Space mt={5} />
|
|
||||||
{sonarrFiltered.map((media: any, index: number) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
<SonarrMediaDisplay media={media} />
|
|
||||||
{index < sonarrFiltered.length - 1 && <Divider variant="dashed" size="sm" my="xl" />}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
{radarrFiltered.length > 0 && sonarrFiltered.length > 0 && (
|
|
||||||
<Divider variant="dashed" size="sm" my="xl" />
|
|
||||||
)}
|
|
||||||
{radarrFiltered.map((media: any, index: number) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
<RadarrMediaDisplay media={media} />
|
|
||||||
{index < radarrFiltered.length - 1 && <Divider variant="dashed" size="sm" my="xl" />}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
{sonarrFiltered.length > 0 && lidarrFiltered.length > 0 && (
|
|
||||||
<Divider variant="dashed" size="sm" my="xl" />
|
|
||||||
)}
|
|
||||||
{lidarrFiltered.map((media: any, index: number) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
<LidarrMediaDisplay media={media} />
|
|
||||||
{index < lidarrFiltered.length - 1 && <Divider variant="dashed" size="sm" my="xl" />}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
{lidarrFiltered.length > 0 && readarrFiltered.length > 0 && (
|
|
||||||
<Divider variant="dashed" size="sm" my="xl" />
|
|
||||||
)}
|
|
||||||
{readarrFiltered.map((media: any, index: number) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
<ReadarrMediaDisplay media={media} />
|
|
||||||
{index < readarrFiltered.length - 1 && <Divider variant="dashed" size="sm" my="xl" />}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</ScrollArea>
|
|
||||||
</Popover.Dropdown>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { CalendarModule } from './CalendarModule';
|
|
||||||
@@ -2,9 +2,8 @@ import { Badge, Button, Group, Image, Stack, Text, Title } from '@mantine/core';
|
|||||||
import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons';
|
import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { useColorTheme } from '../../tools/color';
|
import { useColorTheme } from '../../tools/color';
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { serviceItem } from '../../tools/types';
|
|
||||||
import { RequestModal } from '../overseerr/RequestModal';
|
import { RequestModal } from '../overseerr/RequestModal';
|
||||||
import { Result } from '../overseerr/SearchResult';
|
import { Result } from '../overseerr/SearchResult';
|
||||||
|
|
||||||
@@ -27,9 +26,15 @@ export interface IMedia {
|
|||||||
|
|
||||||
export function OverseerrMediaDisplay(props: any) {
|
export function OverseerrMediaDisplay(props: any) {
|
||||||
const { media }: { media: Result } = props;
|
const { media }: { media: Result } = props;
|
||||||
const { config } = useConfig();
|
const { config } = useConfigContext();
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const service = config.apps.find(
|
const service = config.apps.find(
|
||||||
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
(service) =>
|
||||||
|
service.integration.type === 'overseerr' || service.integration.type === 'jellyseerr'
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -45,9 +50,9 @@ export function OverseerrMediaDisplay(props: any) {
|
|||||||
plexUrl: media.mediaInfo?.plexUrl ?? media.mediaInfo?.mediaUrl,
|
plexUrl: media.mediaInfo?.plexUrl ?? media.mediaInfo?.mediaUrl,
|
||||||
voteAverage: media.voteAverage?.toString(),
|
voteAverage: media.voteAverage?.toString(),
|
||||||
overseerrResult: media,
|
overseerrResult: media,
|
||||||
overseerrId: `${service?.openedUrl ? service?.openedUrl : service?.url}/${
|
overseerrId: `${
|
||||||
media.mediaType
|
service?.behaviour.externalUrl ? service.behaviour.externalUrl : service?.url
|
||||||
}/${media.id}`,
|
}/${media.mediaType}/${media.id}`,
|
||||||
type: 'overseer',
|
type: 'overseer',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -56,16 +61,21 @@ export function OverseerrMediaDisplay(props: any) {
|
|||||||
|
|
||||||
export function ReadarrMediaDisplay(props: any) {
|
export function ReadarrMediaDisplay(props: any) {
|
||||||
const { media }: { media: any } = props;
|
const { media }: { media: any } = props;
|
||||||
const { config } = useConfig();
|
const { config } = useConfigContext();
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Find lidarr in services
|
// Find lidarr in services
|
||||||
const readarr = config.apps.find((service: serviceItem) => service.type === 'Readarr');
|
const readarr = config.apps.find((service) => service.integration.type === 'readarr');
|
||||||
// Find a poster CoverType
|
// Find a poster CoverType
|
||||||
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
||||||
if (!readarr) {
|
if (!readarr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const baseUrl = readarr.openedUrl
|
const baseUrl = readarr.behaviour.externalUrl
|
||||||
? new URL(readarr.openedUrl).origin
|
? new URL(readarr.behaviour.externalUrl).origin
|
||||||
: new URL(readarr.url).origin;
|
: new URL(readarr.url).origin;
|
||||||
// Remove '/' from the end of the lidarr url
|
// Remove '/' from the end of the lidarr url
|
||||||
const fullLink = poster ? `${baseUrl}${poster.url}` : undefined;
|
const fullLink = poster ? `${baseUrl}${poster.url}` : undefined;
|
||||||
@@ -88,15 +98,22 @@ export function ReadarrMediaDisplay(props: any) {
|
|||||||
|
|
||||||
export function LidarrMediaDisplay(props: any) {
|
export function LidarrMediaDisplay(props: any) {
|
||||||
const { media }: { media: any } = props;
|
const { media }: { media: any } = props;
|
||||||
const { config } = useConfig();
|
const { config } = useConfigContext();
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Find lidarr in services
|
// Find lidarr in services
|
||||||
const lidarr = config.apps.find((service: serviceItem) => service.type === 'Lidarr');
|
const lidarr = config.apps.find((service) => service.integration.type === 'lidarr');
|
||||||
// Find a poster CoverType
|
// Find a poster CoverType
|
||||||
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
||||||
if (!lidarr) {
|
if (!lidarr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const baseUrl = lidarr.openedUrl ? new URL(lidarr.openedUrl).origin : new URL(lidarr.url).origin;
|
const baseUrl = lidarr.behaviour.externalUrl
|
||||||
|
? new URL(lidarr.behaviour.externalUrl).origin
|
||||||
|
: new URL(lidarr.url).origin;
|
||||||
// Remove '/' from the end of the lidarr url
|
// Remove '/' from the end of the lidarr url
|
||||||
const fullLink = poster ? `${baseUrl}${poster.url}` : undefined;
|
const fullLink = poster ? `${baseUrl}${poster.url}` : undefined;
|
||||||
// Return a movie poster containting the title and the description
|
// Return a movie poster containting the title and the description
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
import { createStyles, Stack, Title, useMantineColorScheme, useMantineTheme } from '@mantine/core';
|
|
||||||
import { IconCalendar as CalendarIcon } from '@tabler/icons';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { serviceItem } from '../../tools/types';
|
|
||||||
import { IModule } from '../ModuleTypes';
|
|
||||||
|
|
||||||
const asModule = <T extends IModule>(t: T) => t;
|
|
||||||
export const DashdotModule = asModule({
|
|
||||||
title: 'Dash.',
|
|
||||||
icon: CalendarIcon,
|
|
||||||
component: DashdotComponent,
|
|
||||||
options: {
|
|
||||||
cpuMultiView: {
|
|
||||||
name: 'descriptor.settings.cpuMultiView.label',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
storageMultiView: {
|
|
||||||
name: 'descriptor.settings.storageMultiView.label',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
useCompactView: {
|
|
||||||
name: 'descriptor.settings.useCompactView.label',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
graphs: {
|
|
||||||
name: 'descriptor.settings.graphs.label',
|
|
||||||
value: ['CPU', 'RAM', 'Storage', 'Network'],
|
|
||||||
options: ['CPU', 'RAM', 'Storage', 'Network', 'GPU'],
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
name: 'descriptor.settings.url.label',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'dashdot',
|
|
||||||
});
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme, _params, getRef) => ({
|
|
||||||
heading: {
|
|
||||||
marginTop: 0,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
display: 'table',
|
|
||||||
},
|
|
||||||
tableRow: {
|
|
||||||
display: 'table-row',
|
|
||||||
},
|
|
||||||
tableLabel: {
|
|
||||||
display: 'table-cell',
|
|
||||||
paddingRight: 10,
|
|
||||||
},
|
|
||||||
tableValue: {
|
|
||||||
display: 'table-cell',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
paddingBottom: 5,
|
|
||||||
},
|
|
||||||
graphsContainer: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
rowGap: 10,
|
|
||||||
columnGap: 10,
|
|
||||||
},
|
|
||||||
iframe: {
|
|
||||||
flex: '1 0 auto',
|
|
||||||
maxWidth: '100%',
|
|
||||||
height: '140px',
|
|
||||||
borderRadius: theme.radius.lg,
|
|
||||||
border: 'none',
|
|
||||||
colorScheme: 'none',
|
|
||||||
},
|
|
||||||
graphTitle: {
|
|
||||||
ref: getRef('graphTitle'),
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
opacity: 0,
|
|
||||||
transition: 'opacity .1s ease-in-out',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
},
|
|
||||||
graphStack: {
|
|
||||||
[`&:hover .${getRef('graphTitle')}`]: {
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const bpsPrettyPrint = (bits?: number) =>
|
|
||||||
!bits
|
|
||||||
? '-'
|
|
||||||
: bits > 1000 * 1000 * 1000
|
|
||||||
? `${(bits / 1000 / 1000 / 1000).toFixed(1)} Gb/s`
|
|
||||||
: bits > 1000 * 1000
|
|
||||||
? `${(bits / 1000 / 1000).toFixed(1)} Mb/s`
|
|
||||||
: bits > 1000
|
|
||||||
? `${(bits / 1000).toFixed(1)} Kb/s`
|
|
||||||
: `${bits.toFixed(1)} b/s`;
|
|
||||||
|
|
||||||
const bytePrettyPrint = (byte: number): string =>
|
|
||||||
byte > 1024 * 1024 * 1024
|
|
||||||
? `${(byte / 1024 / 1024 / 1024).toFixed(1)} GiB`
|
|
||||||
: byte > 1024 * 1024
|
|
||||||
? `${(byte / 1024 / 1024).toFixed(1)} MiB`
|
|
||||||
: byte > 1024
|
|
||||||
? `${(byte / 1024).toFixed(1)} KiB`
|
|
||||||
: `${byte.toFixed(1)} B`;
|
|
||||||
|
|
||||||
const useJson = (targetUrl: string, url: string) => {
|
|
||||||
const [data, setData] = useState<any | undefined>();
|
|
||||||
|
|
||||||
const doRequest = async () => {
|
|
||||||
try {
|
|
||||||
const resp = await axios.get('/api/modules/dashdot', { params: { url, base: targetUrl } });
|
|
||||||
|
|
||||||
setData(resp.data);
|
|
||||||
// eslint-disable-next-line no-empty
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (targetUrl) {
|
|
||||||
doRequest();
|
|
||||||
}
|
|
||||||
}, [targetUrl]);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DashdotComponent() {
|
|
||||||
const { config } = useConfig();
|
|
||||||
const theme = useMantineTheme();
|
|
||||||
const { classes } = useStyles();
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
|
|
||||||
const dashConfig = config.modules?.[DashdotModule.id].options as typeof DashdotModule['options'];
|
|
||||||
const isCompact = dashConfig?.useCompactView?.value ?? false;
|
|
||||||
const dashdotService: serviceItem | undefined = config.apps.filter(
|
|
||||||
(service) => service.type === 'Dash.'
|
|
||||||
)[0];
|
|
||||||
const dashdotUrl = dashdotService?.url ?? dashConfig?.url?.value ?? '';
|
|
||||||
const enabledGraphs = dashConfig?.graphs?.value ?? ['CPU', 'RAM', 'Storage', 'Network'];
|
|
||||||
const cpuEnabled = enabledGraphs.includes('CPU');
|
|
||||||
const storageEnabled = enabledGraphs.includes('Storage');
|
|
||||||
const ramEnabled = enabledGraphs.includes('RAM');
|
|
||||||
const networkEnabled = enabledGraphs.includes('Network');
|
|
||||||
const gpuEnabled = enabledGraphs.includes('GPU');
|
|
||||||
|
|
||||||
const info = useJson(dashdotUrl, '/info');
|
|
||||||
const storageLoad = useJson(dashdotUrl, '/load/storage');
|
|
||||||
|
|
||||||
const totalUsed =
|
|
||||||
(storageLoad?.layout as any[])?.reduce((acc, curr) => (curr.load ?? 0) + acc, 0) ?? 0;
|
|
||||||
const totalSize =
|
|
||||||
(info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0;
|
|
||||||
|
|
||||||
const { t } = useTranslation('modules/dashdot');
|
|
||||||
|
|
||||||
const graphs = [
|
|
||||||
{
|
|
||||||
id: 'cpu',
|
|
||||||
name: t('card.graphs.cpu.title'),
|
|
||||||
enabled: cpuEnabled,
|
|
||||||
params: {
|
|
||||||
multiView: dashConfig?.cpuMultiView?.value ?? false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'storage',
|
|
||||||
name: t('card.graphs.storage.title'),
|
|
||||||
enabled: storageEnabled && !isCompact,
|
|
||||||
params: {
|
|
||||||
multiView: dashConfig?.storageMultiView?.value ?? false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ram',
|
|
||||||
name: t('card.graphs.memory.title'),
|
|
||||||
enabled: ramEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'network',
|
|
||||||
name: t('card.graphs.network.title'),
|
|
||||||
enabled: networkEnabled,
|
|
||||||
spanTwo: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gpu',
|
|
||||||
name: t('card.graphs.gpu.title'),
|
|
||||||
enabled: gpuEnabled,
|
|
||||||
spanTwo: true,
|
|
||||||
},
|
|
||||||
].filter((g) => g.enabled);
|
|
||||||
|
|
||||||
if (dashdotUrl === '') {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className={classes.heading}>{t('card.title')}</h2>
|
|
||||||
<p>{t('card.errors.noService')}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className={classes.heading}>{t('card.title')}</h2>
|
|
||||||
|
|
||||||
{!info ? (
|
|
||||||
<p>{t('card.errors.noInformation')}</p>
|
|
||||||
) : (
|
|
||||||
<div className={classes.graphsContainer}>
|
|
||||||
<div className={classes.table}>
|
|
||||||
{storageEnabled && isCompact && (
|
|
||||||
<div className={classes.tableRow}>
|
|
||||||
<p className={classes.tableLabel}>{t('card.graphs.storage.label')}</p>
|
|
||||||
<p className={classes.tableValue}>
|
|
||||||
{((100 * totalUsed) / (totalSize || 1)).toFixed(1)}%{'\n'}
|
|
||||||
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{networkEnabled && (
|
|
||||||
<div className={classes.tableRow}>
|
|
||||||
<p className={classes.tableLabel}>{t('card.graphs.network.label')}</p>
|
|
||||||
<p className={classes.tableValue}>
|
|
||||||
{bpsPrettyPrint(info?.network?.speedUp)} {t('card.graphs.network.metrics.upload')}
|
|
||||||
{'\n'}
|
|
||||||
{bpsPrettyPrint(info?.network?.speedDown)}
|
|
||||||
{t('card.graphs.network.metrics.download')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{graphs.map((graph) => (
|
|
||||||
<Stack
|
|
||||||
className={classes.graphStack}
|
|
||||||
style={
|
|
||||||
isCompact
|
|
||||||
? {
|
|
||||||
width: graph.spanTwo ? '100%' : 'calc(50% - 5px)',
|
|
||||||
position: 'relative',
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Title className={classes.graphTitle} order={4} mt={10} mr={25}>
|
|
||||||
{graph.name}
|
|
||||||
</Title>
|
|
||||||
<iframe
|
|
||||||
className={classes.iframe}
|
|
||||||
key={graph.name}
|
|
||||||
title={graph.name}
|
|
||||||
src={`${dashdotUrl}?singleGraphMode=true&graph=${graph.id.toLowerCase()}&theme=${colorScheme}&surface=${(colorScheme ===
|
|
||||||
'dark'
|
|
||||||
? theme.colors.dark[7]
|
|
||||||
: theme.colors.gray[0]
|
|
||||||
).substring(1)}${isCompact ? '&gap=10' : '&gap=5'}&innerRadius=${theme.radius.lg}${
|
|
||||||
graph.params
|
|
||||||
? `&${Object.entries(graph.params)
|
|
||||||
.map(([key, value]) => `${key}=${value.toString()}`)
|
|
||||||
.join('&')}`
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
allowTransparency
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DashdotModule } from './DashdotModule';
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Group, Text, Title } from '@mantine/core';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { IconClock as Clock } from '@tabler/icons';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { IModule } from '../ModuleTypes';
|
|
||||||
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
|
||||||
|
|
||||||
export const DateModule: IModule = {
|
|
||||||
title: 'Date',
|
|
||||||
icon: Clock,
|
|
||||||
component: DateComponent,
|
|
||||||
options: {
|
|
||||||
full: {
|
|
||||||
name: 'descriptor.settings.display24HourFormat.label',
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'date',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DateComponent(props: any) {
|
|
||||||
const [date, setDate] = useState(new Date());
|
|
||||||
const setSafeInterval = useSetSafeInterval();
|
|
||||||
const { config } = useConfig();
|
|
||||||
const isFullTime = config?.modules?.[DateModule.id]?.options?.full?.value ?? true;
|
|
||||||
const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
|
|
||||||
// Change date on minute change
|
|
||||||
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
|
||||||
useEffect(() => {
|
|
||||||
setSafeInterval(() => {
|
|
||||||
setDate(new Date());
|
|
||||||
}, 1000 * 60);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group p="sm" spacing="xs">
|
|
||||||
<Title>{dayjs(date).format(formatString)}</Title>
|
|
||||||
<Text size="lg">{dayjs(date).format('dddd, MMMM D')}</Text>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DateModule } from './DateModule';
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { Button, Group } from '@mantine/core';
|
import { Button, Group } from '@mantine/core';
|
||||||
import { closeModal, openModal } from '@mantine/modals';
|
|
||||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||||
import {
|
import {
|
||||||
IconCheck,
|
IconCheck,
|
||||||
@@ -17,9 +16,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TFunction } from 'react-i18next';
|
import { TFunction } from 'react-i18next';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { tryMatchService } from '../../tools/addToHomarr';
|
|
||||||
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { AppType } from '../../types/app';
|
import { AppType } from '../../types/app';
|
||||||
|
|
||||||
let t: TFunction<'modules/docker', undefined>;
|
let t: TFunction<'modules/docker', undefined>;
|
||||||
@@ -71,7 +68,6 @@ export interface ContainerActionBarProps {
|
|||||||
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
|
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
|
||||||
t = useTranslation('modules/docker').t;
|
t = useTranslation('modules/docker').t;
|
||||||
const [isLoading, setisLoading] = useState(false);
|
const [isLoading, setisLoading] = useState(false);
|
||||||
const { config, setConfig } = useConfig();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
@@ -182,7 +178,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
externalUrl: '',
|
externalUrl: '',
|
||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
type: 'wrapper', // TODO: Set the wrapper automatically
|
type: 'sidebar', // TODO: Set the wrapper automatically
|
||||||
|
properties: {
|
||||||
|
location: 'right',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
shape: {
|
shape: {
|
||||||
location: {
|
location: {
|
||||||
@@ -194,6 +193,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
width: 1,
|
width: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
integration: {
|
||||||
|
type: null,
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core';
|
||||||
import axios from 'axios';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import Docker from 'dockerode';
|
|
||||||
import { IconBrandDocker, IconX } from '@tabler/icons';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
|
import { IconBrandDocker, IconX } from '@tabler/icons';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Docker from 'dockerode';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useConfigContext } from '../../config/provider';
|
||||||
|
import { IModule } from '../ModuleTypes';
|
||||||
import ContainerActionBar from './ContainerActionBar';
|
import ContainerActionBar from './ContainerActionBar';
|
||||||
import DockerTable from './DockerTable';
|
import DockerTable from './DockerTable';
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { IModule } from '../ModuleTypes';
|
|
||||||
|
|
||||||
export const DockerModule: IModule = {
|
export const DockerModule: IModule = {
|
||||||
title: 'Docker',
|
title: 'Docker',
|
||||||
@@ -22,17 +22,18 @@ export default function DockerMenuButton(props: any) {
|
|||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [containers, setContainers] = useState<Docker.ContainerInfo[]>([]);
|
const [containers, setContainers] = useState<Docker.ContainerInfo[]>([]);
|
||||||
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
|
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
|
||||||
const { config } = useConfig();
|
const { config } = useConfigContext();
|
||||||
const moduleEnabled = config.modules?.[DockerModule.id]?.enabled ?? false;
|
|
||||||
|
const dockerEnabled = config?.settings.customization.layout.enabledDocker || false;
|
||||||
|
|
||||||
const { t } = useTranslation('modules/docker');
|
const { t } = useTranslation('modules/docker');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload();
|
reload();
|
||||||
}, [config.modules]);
|
}, [config?.settings]);
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
if (!moduleEnabled) {
|
if (!dockerEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -56,8 +57,8 @@ export default function DockerMenuButton(props: any) {
|
|||||||
});
|
});
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
const exists = config.modules?.[DockerModule.id]?.enabled ?? false;
|
|
||||||
if (!exists) {
|
if (!dockerEnabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Check if the user has at least one container
|
// Check if the user has at least one container
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
export * from './calendar';
|
|
||||||
export * from './dashdot';
|
|
||||||
export * from './date';
|
|
||||||
export * from './torrents';
|
|
||||||
export * from './ping';
|
export * from './ping';
|
||||||
export * from './weather';
|
|
||||||
export * from './docker';
|
export * from './docker';
|
||||||
export * from './overseerr';
|
export * from './overseerr';
|
||||||
export * from './usenet';
|
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
Menu,
|
|
||||||
MultiSelect,
|
|
||||||
Switch,
|
|
||||||
TextInput,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconAdjustments } from '@tabler/icons';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useConfig } from '../tools/state';
|
|
||||||
import { IModule } from './ModuleTypes';
|
|
||||||
|
|
||||||
function getItems(module: IModule) {
|
|
||||||
const { config, setConfig } = useConfig();
|
|
||||||
const { t } = useTranslation([`modules/${module.id}`, 'common']);
|
|
||||||
const items: JSX.Element[] = [];
|
|
||||||
if (module.options) {
|
|
||||||
const keys = Object.keys(module.options);
|
|
||||||
const values = Object.values(module.options);
|
|
||||||
// Get the value and the name of the option
|
|
||||||
const types = values.map((v) => typeof v.value);
|
|
||||||
// Loop over all the types with a for each loop
|
|
||||||
types.forEach((type, index) => {
|
|
||||||
const optionName = `${module.id}.${keys[index]}`;
|
|
||||||
const moduleInConfig = config.modules?.[module.id];
|
|
||||||
if (type === 'object') {
|
|
||||||
items.push(
|
|
||||||
<MultiSelect
|
|
||||||
label={t(`${module.options?.[keys[index]].name}`)}
|
|
||||||
data={module.options?.[keys[index]].options ?? []}
|
|
||||||
defaultValue={
|
|
||||||
(moduleInConfig?.options?.[keys[index]]?.value as string[]) ??
|
|
||||||
(values[index].value as string[]) ??
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
searchable
|
|
||||||
onChange={(value) => {
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
modules: {
|
|
||||||
...config.modules,
|
|
||||||
[module.id]: {
|
|
||||||
...moduleInConfig,
|
|
||||||
options: {
|
|
||||||
...moduleInConfig?.options,
|
|
||||||
[keys[index]]: {
|
|
||||||
...moduleInConfig?.options?.[keys[index]],
|
|
||||||
value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (type === 'string') {
|
|
||||||
items.push(
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
modules: {
|
|
||||||
...config.modules,
|
|
||||||
[module.id]: {
|
|
||||||
...config.modules[module.id],
|
|
||||||
options: {
|
|
||||||
...config.modules[module.id].options,
|
|
||||||
[keys[index]]: {
|
|
||||||
...config.modules[module.id].options?.[keys[index]],
|
|
||||||
value: (e.target as any)[0].value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Group noWrap align="end">
|
|
||||||
<TextInput
|
|
||||||
key={optionName}
|
|
||||||
id={optionName}
|
|
||||||
name={optionName}
|
|
||||||
label={t(`${values[index].name}`)}
|
|
||||||
defaultValue={
|
|
||||||
(moduleInConfig?.options?.[keys[index]]?.value as string) ??
|
|
||||||
(values[index].value as string) ??
|
|
||||||
''
|
|
||||||
}
|
|
||||||
onChange={(e) => {}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit">{t('save', { ns: 'common' })}</Button>
|
|
||||||
</Group>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// TODO: Add support for other types
|
|
||||||
if (type === 'boolean') {
|
|
||||||
items.push(
|
|
||||||
<Switch
|
|
||||||
defaultChecked={
|
|
||||||
// Set default checked to the value of the option if it exists
|
|
||||||
(moduleInConfig?.options?.[keys[index]]?.value as boolean) ??
|
|
||||||
(values[index].value as boolean) ??
|
|
||||||
false
|
|
||||||
}
|
|
||||||
key={keys[index]}
|
|
||||||
onClick={(e) => {
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
modules: {
|
|
||||||
...config.modules,
|
|
||||||
[module.id]: {
|
|
||||||
...config.modules[module.id],
|
|
||||||
options: {
|
|
||||||
...config.modules[module.id].options,
|
|
||||||
[keys[index]]: {
|
|
||||||
...config.modules[module.id].options?.[keys[index]],
|
|
||||||
value: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
label={t(values[index].name)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModuleWrapper(props: any) {
|
|
||||||
const { module }: { module: IModule } = props;
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const { config, setConfig } = useConfig();
|
|
||||||
const enabledModules = config.modules ?? {};
|
|
||||||
// Remove 'Module' from enabled modules titles
|
|
||||||
const isShown = enabledModules[module.id]?.enabled ?? false;
|
|
||||||
//TODO: fix the hover problem
|
|
||||||
const [hovering, setHovering] = useState(false);
|
|
||||||
const { t } = useTranslation('modules');
|
|
||||||
|
|
||||||
if (!isShown) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
{...props}
|
|
||||||
key={module.id}
|
|
||||||
hidden={!isShown}
|
|
||||||
withBorder
|
|
||||||
radius="lg"
|
|
||||||
shadow="sm"
|
|
||||||
style={{
|
|
||||||
background: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '255, 255, 255,'} \
|
|
||||||
${(config.settings.appOpacity || 100) / 100}`,
|
|
||||||
borderColor: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '233, 236, 239,'} \
|
|
||||||
${(config.settings.appOpacity || 100) / 100}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
onHoverStart={() => {
|
|
||||||
setHovering(true);
|
|
||||||
}}
|
|
||||||
onHoverEnd={() => {
|
|
||||||
setHovering(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModuleMenu module={module} hovered={hovering} />
|
|
||||||
<module.component />
|
|
||||||
</motion.div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModuleMenuProps {
|
|
||||||
hovered: boolean;
|
|
||||||
module: IModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModuleMenu(props: ModuleMenuProps) {
|
|
||||||
const { module, hovered } = props;
|
|
||||||
const items: JSX.Element[] = getItems(module);
|
|
||||||
const { t } = useTranslation('modules/common');
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{module.options && (
|
|
||||||
<Menu
|
|
||||||
key={module.id}
|
|
||||||
withinPortal
|
|
||||||
width="lg"
|
|
||||||
shadow="xl"
|
|
||||||
withArrow
|
|
||||||
closeOnItemClick={false}
|
|
||||||
radius="md"
|
|
||||||
position="left"
|
|
||||||
>
|
|
||||||
<Menu.Target>
|
|
||||||
<motion.div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: module.padding?.top ?? 15,
|
|
||||||
right: module.padding?.right ?? 15,
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
zIndex: 10,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: hovered ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon>
|
|
||||||
<IconAdjustments />
|
|
||||||
</ActionIcon>
|
|
||||||
</motion.div>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown style={{ maxWidth: 300 }}>
|
|
||||||
<Menu.Label>{t('settings.label')}</Menu.Label>
|
|
||||||
{items.map((item) => (
|
|
||||||
<Menu.Item key={item.key}>{item}</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Indicator, Tooltip } from '@mantine/core';
|
import { Indicator, Tooltip } from '@mantine/core';
|
||||||
|
import { IconPlug as Plug } from '@tabler/icons';
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { IconPlug as Plug } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { IModule } from '../ModuleTypes';
|
import { IModule } from '../ModuleTypes';
|
||||||
|
|
||||||
export const PingModule: IModule = {
|
export const PingModule: IModule = {
|
||||||
@@ -16,12 +16,12 @@ export const PingModule: IModule = {
|
|||||||
|
|
||||||
export default function PingComponent(props: any) {
|
export default function PingComponent(props: any) {
|
||||||
type State = 'loading' | 'down' | 'online';
|
type State = 'loading' | 'down' | 'online';
|
||||||
const { config } = useConfig();
|
const { config } = useConfigContext();
|
||||||
|
|
||||||
const { url }: { url: string } = props;
|
const { url }: { url: string } = props;
|
||||||
const [isOnline, setOnline] = useState<State>('loading');
|
const [isOnline, setOnline] = useState<State>('loading');
|
||||||
const [response, setResponse] = useState(500);
|
const [response, setResponse] = useState(500);
|
||||||
const exists = config.modules?.[PingModule.id]?.enabled ?? false;
|
const exists = config?.settings.customization.layout.enabledPing || false;
|
||||||
|
|
||||||
const { t } = useTranslation('modules/ping');
|
const { t } = useTranslation('modules/ping');
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export default function PingComponent(props: any) {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
statusCheck(error.response);
|
statusCheck(error.response);
|
||||||
});
|
});
|
||||||
}, [config.modules?.[PingModule.id]?.enabled]);
|
}, [config?.settings.customization.layout.enabledPing]);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
import {
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
Title,
|
|
||||||
Group,
|
|
||||||
Progress,
|
|
||||||
Skeleton,
|
|
||||||
ScrollArea,
|
|
||||||
Center,
|
|
||||||
Stack,
|
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconDownload as Download } from '@tabler/icons';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useElementSize } from '@mantine/hooks';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { IModule } from '../ModuleTypes';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
|
||||||
|
|
||||||
export const TorrentsModule: IModule = {
|
|
||||||
id: 'torrents-status',
|
|
||||||
title: 'Torrent',
|
|
||||||
icon: Download,
|
|
||||||
component: TorrentsComponent,
|
|
||||||
options: {
|
|
||||||
hideComplete: {
|
|
||||||
name: 'descriptor.settings.hideComplete.label',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TorrentsComponent() {
|
|
||||||
const { config } = useConfig();
|
|
||||||
const downloadServices =
|
|
||||||
config.apps.filter(
|
|
||||||
(service) =>
|
|
||||||
service.type === 'qBittorrent' ||
|
|
||||||
service.type === 'Transmission' ||
|
|
||||||
service.type === 'Deluge'
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
const hideComplete: boolean =
|
|
||||||
(config?.modules?.[TorrentsModule.id]?.options?.hideComplete?.value as boolean) ?? false;
|
|
||||||
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);
|
|
||||||
const setSafeInterval = useSetSafeInterval();
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const { ref, width, height } = useElementSize();
|
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
|
||||||
|
|
||||||
const { t } = useTranslation(`modules/${TorrentsModule.id}`);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsLoading(true);
|
|
||||||
if (downloadServices.length === 0) return;
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
// Send one request with each download service inside
|
|
||||||
axios
|
|
||||||
.post('/api/modules/torrents')
|
|
||||||
.then((response) => {
|
|
||||||
setTorrents(response.data);
|
|
||||||
setIsLoading(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setTorrents([]);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Error while fetching torrents', error.response.data);
|
|
||||||
setIsLoading(false);
|
|
||||||
showNotification({
|
|
||||||
title: 'Error fetching torrents',
|
|
||||||
autoClose: 1000,
|
|
||||||
disallowClose: true,
|
|
||||||
id: 'fail-torrent-downloads-module',
|
|
||||||
color: 'red',
|
|
||||||
message:
|
|
||||||
'Please check your config for any potential errors, check the console for more info',
|
|
||||||
});
|
|
||||||
clearInterval(interval);
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (downloadServices.length === 0) {
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
|
||||||
<Group>
|
|
||||||
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Skeleton height={40} mt={10} />
|
|
||||||
<Skeleton height={40} mt={10} />
|
|
||||||
<Skeleton height={40} mt={10} />
|
|
||||||
<Skeleton height={40} mt={10} />
|
|
||||||
<Skeleton height={40} mt={10} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const ths = (
|
|
||||||
<tr ref={ref}>
|
|
||||||
<th>{t('card.table.header.name')}</th>
|
|
||||||
<th>{t('card.table.header.size')}</th>
|
|
||||||
{width > MIN_WIDTH_MOBILE && <th>{t('card.table.header.download')}</th>}
|
|
||||||
{width > MIN_WIDTH_MOBILE && <th>{t('card.table.header.upload')}</th>}
|
|
||||||
{width > MIN_WIDTH_MOBILE && <th>{t('card.table.header.estimatedTimeOfArrival')}</th>}
|
|
||||||
<th>{t('card.table.header.progress')}</th>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
// Convert Seconds to readable format.
|
|
||||||
function calculateETA(givenSeconds: number) {
|
|
||||||
// If its superior than one day return > 1 day
|
|
||||||
if (givenSeconds > 86400) {
|
|
||||||
return '> 1 day';
|
|
||||||
}
|
|
||||||
// Transform the givenSeconds into a readable format. e.g. 1h 2m 3s
|
|
||||||
const hours = Math.floor(givenSeconds / 3600);
|
|
||||||
const minutes = Math.floor((givenSeconds % 3600) / 60);
|
|
||||||
const seconds = Math.floor(givenSeconds % 60);
|
|
||||||
// Only show hours if it's greater than 0.
|
|
||||||
const hoursString = hours > 0 ? `${hours}h ` : '';
|
|
||||||
const minutesString = minutes > 0 ? `${minutes}m ` : '';
|
|
||||||
const secondsString = seconds > 0 ? `${seconds}s` : '';
|
|
||||||
return `${hoursString}${minutesString}${secondsString}`;
|
|
||||||
}
|
|
||||||
// Loop over qBittorrent torrents merging with deluge torrents
|
|
||||||
const rows = torrents
|
|
||||||
.filter((torrent) => !(torrent.progress === 1 && hideComplete))
|
|
||||||
.map((torrent) => {
|
|
||||||
const downloadSpeed = torrent.downloadSpeed / 1024 / 1024;
|
|
||||||
const uploadSpeed = torrent.uploadSpeed / 1024 / 1024;
|
|
||||||
const size = torrent.totalSelected;
|
|
||||||
return (
|
|
||||||
<tr key={torrent.id}>
|
|
||||||
<td>
|
|
||||||
<Tooltip position="top" label={torrent.name}>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
maxWidth: '30vw',
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
lineClamp={1}
|
|
||||||
>
|
|
||||||
{torrent.name}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Text size="xs">{humanFileSize(size)}</Text>
|
|
||||||
</td>
|
|
||||||
{width > MIN_WIDTH_MOBILE && (
|
|
||||||
<td>
|
|
||||||
<Text size="xs">{downloadSpeed > 0 ? `${downloadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{width > MIN_WIDTH_MOBILE && (
|
|
||||||
<td>
|
|
||||||
<Text size="xs">{uploadSpeed > 0 ? `${uploadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{width > MIN_WIDTH_MOBILE && (
|
|
||||||
<td>
|
|
||||||
<Text size="xs">{torrent.eta <= 0 ? '∞' : calculateETA(torrent.eta)}</Text>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
<td>
|
|
||||||
<Text>{(torrent.progress * 100).toFixed(1)}%</Text>
|
|
||||||
<Progress
|
|
||||||
radius="lg"
|
|
||||||
color={
|
|
||||||
torrent.progress === 1 ? 'green' : torrent.state === 'paused' ? 'yellow' : 'blue'
|
|
||||||
}
|
|
||||||
value={torrent.progress * 100}
|
|
||||||
size="lg"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea sx={{ height: 300, width: '100%' }}>
|
|
||||||
{rows.length > 0 ? (
|
|
||||||
<Table highlightOnHover p="sm">
|
|
||||||
<thead>{ths}</thead>
|
|
||||||
<tbody>{rows}</tbody>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
<Title order={3}>{t('card.table.body.nothingFound')}</Title>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { Text, Title, Group, useMantineTheme, Box, Card, ColorSwatch, Stack } from '@mantine/core';
|
|
||||||
import { IconDownload as Download } from '@tabler/icons';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
|
||||||
import { linearGradientDef } from '@nivo/core';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { Datum, ResponsiveLine } from '@nivo/line';
|
|
||||||
import { useListState } from '@mantine/hooks';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
|
||||||
import { IModule } from '../ModuleTypes';
|
|
||||||
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
|
||||||
|
|
||||||
export const TotalDownloadsModule: IModule = {
|
|
||||||
title: 'Download Speed',
|
|
||||||
icon: Download,
|
|
||||||
component: TotalDownloadsComponent,
|
|
||||||
id: 'dlspeed',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface torrentHistory {
|
|
||||||
x: number;
|
|
||||||
up: number;
|
|
||||||
down: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TotalDownloadsComponent() {
|
|
||||||
const setSafeInterval = useSetSafeInterval();
|
|
||||||
const { config } = useConfig();
|
|
||||||
const downloadServices =
|
|
||||||
config.apps.filter(
|
|
||||||
(service) =>
|
|
||||||
service.type === 'qBittorrent' ||
|
|
||||||
service.type === 'Transmission' ||
|
|
||||||
service.type === 'Deluge'
|
|
||||||
) ?? [];
|
|
||||||
const { t } = useTranslation(`modules/${TotalDownloadsModule.id}`);
|
|
||||||
|
|
||||||
const [torrentHistory, torrentHistoryHandlers] = useListState<torrentHistory>([]);
|
|
||||||
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);
|
|
||||||
|
|
||||||
const totalDownloadSpeed = torrents.reduce((acc, torrent) => acc + torrent.downloadSpeed, 0);
|
|
||||||
const totalUploadSpeed = torrents.reduce((acc, torrent) => acc + torrent.uploadSpeed, 0);
|
|
||||||
useEffect(() => {
|
|
||||||
if (downloadServices.length === 0) return;
|
|
||||||
const interval = setSafeInterval(() => {
|
|
||||||
// Send one request with each download service inside
|
|
||||||
axios
|
|
||||||
.post('/api/modules/torrents')
|
|
||||||
.then((response) => {
|
|
||||||
setTorrents(response.data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setTorrents([]);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Error while fetching torrents', error.response.data);
|
|
||||||
showNotification({
|
|
||||||
title: 'Torrent speed module failed to fetch torrents',
|
|
||||||
autoClose: 1000,
|
|
||||||
disallowClose: true,
|
|
||||||
id: 'fail-torrent-speed-module',
|
|
||||||
color: 'red',
|
|
||||||
message:
|
|
||||||
'Error fetching torrents, please check your config for any potential errors, check the console for more info',
|
|
||||||
});
|
|
||||||
clearInterval(interval);
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}, [config.apps]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
torrentHistoryHandlers.append({
|
|
||||||
x: Date.now(),
|
|
||||||
down: totalDownloadSpeed,
|
|
||||||
up: totalUploadSpeed,
|
|
||||||
});
|
|
||||||
}, [totalDownloadSpeed, totalUploadSpeed]);
|
|
||||||
|
|
||||||
if (downloadServices.length === 0) {
|
|
||||||
return (
|
|
||||||
<Group>
|
|
||||||
<Title order={4}>{t('card.errors.noDownloadClients.title')}</Title>
|
|
||||||
<div>{t('card.errors.noDownloadClients.text')}</div>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = useMantineTheme();
|
|
||||||
// Load the last 10 values from the history
|
|
||||||
const history = torrentHistory.slice(-10);
|
|
||||||
const chartDataUp = history.map((load, i) => ({
|
|
||||||
x: load.x,
|
|
||||||
y: load.up,
|
|
||||||
})) as Datum[];
|
|
||||||
const chartDataDown = history.map((load, i) => ({
|
|
||||||
x: load.x,
|
|
||||||
y: load.down,
|
|
||||||
})) as Datum[];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Title order={4}>{t('card.lineChart.title')}</Title>
|
|
||||||
<Stack>
|
|
||||||
<Group>
|
|
||||||
<ColorSwatch size={12} color={theme.colors.green[5]} />
|
|
||||||
<Text>
|
|
||||||
{t('card.lineChart.totalDownload', { download: humanFileSize(totalDownloadSpeed) })}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group>
|
|
||||||
<ColorSwatch size={12} color={theme.colors.blue[5]} />
|
|
||||||
<Text>
|
|
||||||
{t('card.lineChart.totalUpload', { upload: humanFileSize(totalUploadSpeed) })}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
height: 200,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ResponsiveLine
|
|
||||||
isInteractive
|
|
||||||
enableSlices="x"
|
|
||||||
sliceTooltip={({ slice }) => {
|
|
||||||
const Download = slice.points[0].data.y as number;
|
|
||||||
const Upload = slice.points[1].data.y as number;
|
|
||||||
// Get the number of seconds since the last update.
|
|
||||||
const seconds = (Date.now() - (slice.points[0].data.x as number)) / 1000;
|
|
||||||
// Round to the nearest second.
|
|
||||||
const roundedSeconds = Math.round(seconds);
|
|
||||||
return (
|
|
||||||
<Card p="sm" radius="md" withBorder>
|
|
||||||
<Text size="md">{t('card.lineChart.timeSpan', { seconds: roundedSeconds })}</Text>
|
|
||||||
<Card.Section p="sm">
|
|
||||||
<Stack>
|
|
||||||
<Group>
|
|
||||||
<ColorSwatch size={10} color={theme.colors.green[5]} />
|
|
||||||
<Text size="md">
|
|
||||||
{t('card.lineChart.download', { download: humanFileSize(Download) })}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group>
|
|
||||||
<ColorSwatch size={10} color={theme.colors.blue[5]} />
|
|
||||||
<Text size="md">
|
|
||||||
{t('card.lineChart.upload', { upload: humanFileSize(Upload) })}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Card.Section>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
id: 'downloads',
|
|
||||||
data: chartDataUp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'uploads',
|
|
||||||
data: chartDataDown,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
curve="monotoneX"
|
|
||||||
yFormat=" >-.2f"
|
|
||||||
axisTop={null}
|
|
||||||
axisRight={null}
|
|
||||||
enablePoints={false}
|
|
||||||
animate={false}
|
|
||||||
enableGridX={false}
|
|
||||||
enableGridY={false}
|
|
||||||
enableArea
|
|
||||||
defs={[
|
|
||||||
linearGradientDef('gradientA', [
|
|
||||||
{ offset: 0, color: 'inherit' },
|
|
||||||
{ offset: 100, color: 'inherit', opacity: 0 },
|
|
||||||
]),
|
|
||||||
]}
|
|
||||||
fill={[{ match: '*', id: 'gradientA' }]}
|
|
||||||
colors={[
|
|
||||||
// Blue
|
|
||||||
theme.colors.blue[5],
|
|
||||||
// Green
|
|
||||||
theme.colors.green[5],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { TorrentsModule } from './TorrentsModule';
|
|
||||||
export { TotalDownloadsModule } from './TotalDownloadsModule';
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import {
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Group,
|
|
||||||
Select,
|
|
||||||
Stack,
|
|
||||||
Tabs,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons';
|
|
||||||
import { FunctionComponent, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import duration from 'dayjs/plugin/duration';
|
|
||||||
import { useElementSize } from '@mantine/hooks';
|
|
||||||
import { IModule } from '../ModuleTypes';
|
|
||||||
import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType';
|
|
||||||
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
|
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
|
||||||
import { UsenetQueueList } from '../../widgets/useNet/UsenetQueueList';
|
|
||||||
import { UsenetHistoryList } from '../../widgets/useNet/UsenetHistoryList';
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
|
||||||
|
|
||||||
export const UsenetComponent: FunctionComponent = () => {
|
|
||||||
const downloadServices = useGetServiceByType('Sabnzbd', 'NZBGet');
|
|
||||||
|
|
||||||
const { t } = useTranslation('modules/usenet');
|
|
||||||
|
|
||||||
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id);
|
|
||||||
const { data } = useGetUsenetInfo({ appId: selectedServiceId! });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedServiceId && downloadServices.length) {
|
|
||||||
setSelectedService(downloadServices[0].id);
|
|
||||||
}
|
|
||||||
}, [downloadServices, selectedServiceId]);
|
|
||||||
|
|
||||||
const { mutate: pause } = usePauseUsenetQueue({ appId: selectedServiceId! });
|
|
||||||
const { mutate: resume } = useResumeUsenetQueue({ appId: selectedServiceId! });
|
|
||||||
|
|
||||||
if (downloadServices.length === 0) {
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
|
||||||
<Group>
|
|
||||||
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedServiceId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ref, width, height } = useElementSize();
|
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs keepMounted={false} defaultValue="queue">
|
|
||||||
<Tabs.List ref={ref} mb="md" style={{ flex: 1 }}>
|
|
||||||
<Tabs.Tab value="queue">{t('tabs.queue')}</Tabs.Tab>
|
|
||||||
<Tabs.Tab value="history">{t('tabs.history')}</Tabs.Tab>
|
|
||||||
{data && (
|
|
||||||
<Group position="right" ml="auto">
|
|
||||||
{width > MIN_WIDTH_MOBILE && (
|
|
||||||
<>
|
|
||||||
<Badge>{humanFileSize(data?.speed)}/s</Badge>
|
|
||||||
<Badge>
|
|
||||||
{t('info.sizeLeft')}: {humanFileSize(data?.sizeLeft)}
|
|
||||||
</Badge>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.paused ? (
|
|
||||||
<Button uppercase onClick={() => resume()} radius="xl" size="xs">
|
|
||||||
<IconPlayerPlay size={12} style={{ marginRight: 5 }} /> {t('info.paused')}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button uppercase onClick={() => pause()} radius="xl" size="xs">
|
|
||||||
<IconPlayerPause size={12} style={{ marginRight: 5 }} />{' '}
|
|
||||||
{dayjs.duration(data.eta, 's').format('HH:mm')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Tabs.List>
|
|
||||||
{downloadServices.length > 1 && (
|
|
||||||
<Select
|
|
||||||
value={selectedServiceId}
|
|
||||||
onChange={setSelectedService}
|
|
||||||
ml="xs"
|
|
||||||
data={downloadServices.map((service) => ({ value: service.id, label: service.name }))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Tabs.Panel value="queue">
|
|
||||||
<UsenetQueueList appId={selectedServiceId} />
|
|
||||||
</Tabs.Panel>
|
|
||||||
<Tabs.Panel value="history">
|
|
||||||
<UsenetHistoryList appId={selectedServiceId} />
|
|
||||||
</Tabs.Panel>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UsenetModule: IModule = {
|
|
||||||
id: 'usenet',
|
|
||||||
title: 'Usenet',
|
|
||||||
icon: IconDownload,
|
|
||||||
component: UsenetComponent,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UsenetComponent;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { UsenetModule } from './UsenetModule';
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// To parse this data:
|
|
||||||
//
|
|
||||||
// import { Convert, WeatherResponse } from "./file";
|
|
||||||
//
|
|
||||||
// const weatherResponse = Convert.toWeatherResponse(json);
|
|
||||||
//
|
|
||||||
// These functions will throw an error if the JSON doesn't
|
|
||||||
// match the expected interface, even if the JSON is valid.
|
|
||||||
|
|
||||||
export interface WeatherResponse {
|
|
||||||
current_weather: CurrentWeather;
|
|
||||||
utc_offset_seconds: number;
|
|
||||||
latitude: number;
|
|
||||||
elevation: number;
|
|
||||||
longitude: number;
|
|
||||||
generationtime_ms: number;
|
|
||||||
daily_units: DailyUnits;
|
|
||||||
daily: Daily;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CurrentWeather {
|
|
||||||
winddirection: number;
|
|
||||||
windspeed: number;
|
|
||||||
time: string;
|
|
||||||
weathercode: number;
|
|
||||||
temperature: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Daily {
|
|
||||||
temperature_2m_max: number[];
|
|
||||||
time: Date[];
|
|
||||||
temperature_2m_min: number[];
|
|
||||||
weathercode: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DailyUnits {
|
|
||||||
temperature_2m_max: string;
|
|
||||||
temperature_2m_min: string;
|
|
||||||
time: string;
|
|
||||||
weathercode: string;
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
import { Group, Space, Title, Tooltip, Skeleton, Stack, Box } from '@mantine/core';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
IconArrowDownRight as ArrowDownRight,
|
|
||||||
IconArrowUpRight as ArrowUpRight,
|
|
||||||
IconCloud as Cloud,
|
|
||||||
IconCloudFog as CloudFog,
|
|
||||||
IconCloudRain as CloudRain,
|
|
||||||
IconCloudSnow as CloudSnow,
|
|
||||||
IconCloudStorm as CloudStorm,
|
|
||||||
IconQuestionMark as QuestionMark,
|
|
||||||
IconSnowflake as Snowflake,
|
|
||||||
IconSun as Sun,
|
|
||||||
} from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { IModule } from '../ModuleTypes';
|
|
||||||
import { WeatherResponse } from './WeatherInterface';
|
|
||||||
|
|
||||||
export const WeatherModule: IModule = {
|
|
||||||
title: 'Weather',
|
|
||||||
icon: Sun,
|
|
||||||
component: WeatherComponent,
|
|
||||||
options: {
|
|
||||||
freedomunit: {
|
|
||||||
name: 'descriptor.settings.displayInFahrenheit.label',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
name: 'descriptor.settings.location.label',
|
|
||||||
value: 'Paris',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'weather',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 0 Clear sky
|
|
||||||
// 1, 2, 3 Mainly clear, partly cloudy, and overcast
|
|
||||||
// 45, 48 Fog and depositing rime fog
|
|
||||||
// 51, 53, 55 Drizzle: Light, moderate, and dense intensity
|
|
||||||
// 56, 57 Freezing Drizzle: Light and dense intensity
|
|
||||||
// 61, 63, 65 Rain: Slight, moderate and heavy intensity
|
|
||||||
// 66, 67 Freezing Rain: Light and heavy intensity
|
|
||||||
// 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity
|
|
||||||
// 77 Snow grains
|
|
||||||
// 80, 81, 82 Rain showers: Slight, moderate, and violent
|
|
||||||
// 85, 86Snow showers slight and heavy
|
|
||||||
// 95 *Thunderstorm: Slight or moderate
|
|
||||||
// 96, 99 *Thunderstorm with slight and heavy hail
|
|
||||||
export function WeatherIcon(props: any) {
|
|
||||||
const { t } = useTranslation('modules/weather');
|
|
||||||
|
|
||||||
const { code } = props;
|
|
||||||
let data: { icon: any; name: string };
|
|
||||||
switch (code) {
|
|
||||||
case 0: {
|
|
||||||
data = { icon: Sun, name: t('card.weatherDescriptions.clear') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
case 2:
|
|
||||||
case 3: {
|
|
||||||
data = { icon: Cloud, name: t('card.weatherDescriptions.mainlyClear') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 45:
|
|
||||||
case 48: {
|
|
||||||
data = { icon: CloudFog, name: t('card.weatherDescriptions.fog') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 51:
|
|
||||||
case 53:
|
|
||||||
case 55: {
|
|
||||||
data = { icon: Cloud, name: t('card.weatherDescriptions.drizzle') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 56:
|
|
||||||
case 57: {
|
|
||||||
data = {
|
|
||||||
icon: Snowflake,
|
|
||||||
name: t('card.weatherDescriptions.freezingDrizzle'),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 61:
|
|
||||||
case 63:
|
|
||||||
case 65: {
|
|
||||||
data = { icon: CloudRain, name: t('card.weatherDescriptions.rain') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 66:
|
|
||||||
case 67: {
|
|
||||||
data = { icon: CloudRain, name: t('card.weatherDescriptions.freezingRain') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 71:
|
|
||||||
case 73:
|
|
||||||
case 75: {
|
|
||||||
data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowFall') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 77: {
|
|
||||||
data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowGrains') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 80:
|
|
||||||
case 81:
|
|
||||||
case 82: {
|
|
||||||
data = { icon: CloudRain, name: t('card.weatherDescriptions.rainShowers') };
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 85:
|
|
||||||
case 86: {
|
|
||||||
data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowShowers') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 95: {
|
|
||||||
data = { icon: CloudStorm, name: t('card.weatherDescriptions.thunderstorm') };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 96:
|
|
||||||
case 99: {
|
|
||||||
data = {
|
|
||||||
icon: CloudStorm,
|
|
||||||
name: t('card.weatherDescriptions.thunderstormWithHail'),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
data = { icon: QuestionMark, name: t('card.weatherDescriptions.unknown') };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Tooltip withinPortal withArrow label={data.name}>
|
|
||||||
<Box>
|
|
||||||
<data.icon size={50} />
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WeatherComponent(props: any) {
|
|
||||||
// Get location from browser
|
|
||||||
const { config } = useConfig();
|
|
||||||
const [weather, setWeather] = useState({} as WeatherResponse);
|
|
||||||
const cityInput: string =
|
|
||||||
(config?.modules?.[WeatherModule.id]?.options?.location?.value as string) ?? 'Paris';
|
|
||||||
const isFahrenheit: boolean =
|
|
||||||
(config?.modules?.[WeatherModule.id]?.options?.freedomunit?.value as boolean) ?? false;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
axios
|
|
||||||
.get(`https://geocoding-api.open-meteo.com/v1/search?name=${cityInput}`)
|
|
||||||
.then((response) => {
|
|
||||||
// Check if results exists
|
|
||||||
const { latitude, longitude } = response.data.results
|
|
||||||
? response.data.results[0]
|
|
||||||
: { latitude: 0, longitude: 0 };
|
|
||||||
axios
|
|
||||||
.get(
|
|
||||||
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FLondon`
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
setWeather(res.data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [cityInput]);
|
|
||||||
if (!weather.current_weather) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Skeleton height={40} width={100} mb="xl" />
|
|
||||||
<Group noWrap>
|
|
||||||
<Skeleton height={50} circle />
|
|
||||||
<Group>
|
|
||||||
<Skeleton height={25} width={70} mr="lg" />
|
|
||||||
<Skeleton height={25} width={70} />
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function usePerferedUnit(value: number): string {
|
|
||||||
return isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Stack p="sm" spacing="xs">
|
|
||||||
<Title>{usePerferedUnit(weather.current_weather.temperature)}</Title>
|
|
||||||
<Group spacing={0}>
|
|
||||||
<WeatherIcon code={weather.current_weather.weathercode} />
|
|
||||||
<Space mx="sm" />
|
|
||||||
<span>{usePerferedUnit(weather.daily.temperature_2m_max[0])}</span>
|
|
||||||
<ArrowUpRight size={16} style={{ right: 15 }} />
|
|
||||||
<Space mx="sm" />
|
|
||||||
<span>{usePerferedUnit(weather.daily.temperature_2m_min[0])}</span>
|
|
||||||
<ArrowDownRight size={16} />
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { WeatherModule } from './WeatherModule';
|
|
||||||
@@ -4,20 +4,25 @@ import { GetServerSidePropsContext } from 'next';
|
|||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import AppShelf from '../components/AppShelf/AppShelf';
|
|
||||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||||
|
import { Dashboard } from '../components/Dashboard/Dashboard';
|
||||||
import Layout from '../components/layout/Layout';
|
import Layout from '../components/layout/Layout';
|
||||||
|
import { useConfigContext } from '../config/provider';
|
||||||
|
import { useConfigStore } from '../config/store';
|
||||||
import { getConfig } from '../tools/getConfig';
|
import { getConfig } from '../tools/getConfig';
|
||||||
import { useConfig } from '../tools/state';
|
|
||||||
import { dashboardNamespaces } from '../tools/translation-namespaces';
|
import { dashboardNamespaces } from '../tools/translation-namespaces';
|
||||||
import { Config } from '../tools/types';
|
import { ConfigType } from '../types/config';
|
||||||
|
|
||||||
|
type ServerSideProps = {
|
||||||
|
config: ConfigType;
|
||||||
|
};
|
||||||
|
|
||||||
export async function getServerSideProps({
|
export async function getServerSideProps({
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
locale,
|
locale,
|
||||||
query,
|
query,
|
||||||
}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> {
|
}: GetServerSidePropsContext): Promise<{ props: ServerSideProps }> {
|
||||||
const configByUrl = query.slug;
|
const configByUrl = query.slug;
|
||||||
const configPath = path.join(process.cwd(), 'data/configs', `${configByUrl}.json`);
|
const configPath = path.join(process.cwd(), 'data/configs', `${configByUrl}.json`);
|
||||||
const configExists = fs.existsSync(configPath);
|
const configExists = fs.existsSync(configPath);
|
||||||
@@ -28,12 +33,36 @@ export async function getServerSideProps({
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
config: {
|
config: {
|
||||||
name: 'Default config',
|
schemaVersion: '1.0',
|
||||||
|
configProperties: {
|
||||||
|
name: 'Default Configuration',
|
||||||
|
},
|
||||||
apps: [],
|
apps: [],
|
||||||
settings: {
|
settings: {
|
||||||
searchUrl: 'https://www.google.com/search?q=',
|
common: {
|
||||||
|
searchEngine: {
|
||||||
|
type: 'google',
|
||||||
|
properties: {
|
||||||
|
enabled: true,
|
||||||
|
openInNewTab: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultConfig: 'default',
|
||||||
|
},
|
||||||
|
customization: {
|
||||||
|
layout: {
|
||||||
|
enabledLeftSidebar: false,
|
||||||
|
enabledRightSidebar: false,
|
||||||
|
enabledSearchbar: true,
|
||||||
|
enabledDocker: false,
|
||||||
|
enabledPing: false,
|
||||||
|
},
|
||||||
|
colors: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modules: {},
|
categories: [],
|
||||||
|
wrappers: [],
|
||||||
|
widgets: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -46,14 +75,18 @@ export async function getServerSideProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function HomePage(props: any) {
|
export default function HomePage(props: any) {
|
||||||
const { config: initialConfig }: { config: Config } = props;
|
const { config: initialConfig }: { config: ConfigType } = props;
|
||||||
const { setConfig } = useConfig();
|
const { name: configName } = useConfigContext();
|
||||||
|
const { updateConfig } = useConfigStore();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setConfig(initialConfig);
|
if (!configName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateConfig(configName, () => initialConfig);
|
||||||
}, [initialConfig]);
|
}, [initialConfig]);
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<AppShelf />
|
<Dashboard />
|
||||||
<LoadConfigComponent />
|
<LoadConfigComponent />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getCookie, setCookie } from 'cookies-next';
|
import { getCookie, setCookie } from 'cookies-next';
|
||||||
import { GetServerSidePropsContext } from 'next';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
|
import { SSRConfig } from 'next-i18next';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
|
|
||||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||||
@@ -12,7 +13,9 @@ import { ConfigType } from '../types/config';
|
|||||||
|
|
||||||
type ServerSideProps = {
|
type ServerSideProps = {
|
||||||
config: ConfigType;
|
config: ConfigType;
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
configName: string;
|
configName: string;
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
_nextI18Next: SSRConfig['_nextI18Next'];
|
_nextI18Next: SSRConfig['_nextI18Next'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,23 +35,3 @@ export function tryMatchService(container: Dockerode.ContainerInfo | undefined)
|
|||||||
.toLowerCase()}.png`,
|
.toLowerCase()}.png`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function addToHomarr(
|
|
||||||
container: Dockerode.ContainerInfo,
|
|
||||||
config: Config,
|
|
||||||
setConfig: (newconfig: Config) => void
|
|
||||||
) {
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
apps: [
|
|
||||||
...config.apps,
|
|
||||||
{
|
|
||||||
name: container.Names[0].substring(1),
|
|
||||||
id: container.Id,
|
|
||||||
type: tryMatchType(container.Image),
|
|
||||||
url: `localhost:${container.Ports.at(0)?.PublicPort}`,
|
|
||||||
icon: await MatchIcon(container.Names[0].substring(1)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const getFallbackConfig = (name?: string): BackendConfigType => ({
|
|||||||
name: name ?? 'default',
|
name: name ?? 'default',
|
||||||
},
|
},
|
||||||
categories: [],
|
categories: [],
|
||||||
widgets: {},
|
widgets: [],
|
||||||
apps: [],
|
apps: [],
|
||||||
settings: {
|
settings: {
|
||||||
common: {
|
common: {
|
||||||
|
|||||||
@@ -6,18 +6,17 @@ export const getFrontendConfig = (name: string): ConfigType => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
apps: config.apps.map((s) => ({
|
apps: config.apps.map((app) => ({
|
||||||
...s,
|
...app,
|
||||||
integration: s.integration
|
integration: {
|
||||||
? {
|
...app.integration ?? null,
|
||||||
...s.integration,
|
type: app.integration?.type ?? null,
|
||||||
properties: s.integration?.properties.map((p) => ({
|
properties: app.integration?.properties.map((property) => ({
|
||||||
...p,
|
...property,
|
||||||
value: p.type === 'private' ? null : p.value,
|
value: property.type === 'private' ? undefined : property.value,
|
||||||
isDefined: p.value != null,
|
isDefined: property.value != null,
|
||||||
})),
|
})) ?? [],
|
||||||
}
|
},
|
||||||
: null,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { useConfig } from '../state';
|
|
||||||
import { Config, ServiceType } from '../types';
|
|
||||||
|
|
||||||
export const useGetServiceByType = (...serviceTypes: ServiceType[]) => {
|
|
||||||
const { config } = useConfig();
|
|
||||||
|
|
||||||
return getServiceByType(config, ...serviceTypes);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServiceByType = (config: Config, ...serviceTypes: ServiceType[]) =>
|
|
||||||
config.apps.filter((s) => serviceTypes.includes(s.type));
|
|
||||||
|
|
||||||
export const getServiceById = (config: Config, id: string) => config.apps.find((s) => s.id === id);
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { Config } from './types';
|
|
||||||
|
|
||||||
export function migrateToIdConfig(config: Config): Config {
|
|
||||||
// Set the config and add an ID to all the services that don't have one
|
|
||||||
const services = config.apps.map((service) => ({
|
|
||||||
...service,
|
|
||||||
id: service.id ?? uuidv4(),
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
apps: services,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
// src/context/state.js
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
|
||||||
import { IconCheck as Check, IconX as X } from '@tabler/icons';
|
|
||||||
import { Config } from './types';
|
|
||||||
|
|
||||||
type configContextType = {
|
|
||||||
config: Config;
|
|
||||||
setConfig: (newconfig: Config) => void;
|
|
||||||
loadConfig: (name: string) => void;
|
|
||||||
getConfigs: () => Promise<string[]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const configContext = createContext<configContextType>({
|
|
||||||
config: {
|
|
||||||
name: 'default',
|
|
||||||
apps: [],
|
|
||||||
settings: {
|
|
||||||
searchUrl: 'https://google.com/search?q=',
|
|
||||||
},
|
|
||||||
modules: {},
|
|
||||||
},
|
|
||||||
setConfig: () => {},
|
|
||||||
loadConfig: async (name: string) => {},
|
|
||||||
getConfigs: async () => [],
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useConfig() {
|
|
||||||
const context = useContext(configContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useConfig must be used within a ConfigProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ConfigProvider({ children }: Props) {
|
|
||||||
const [config, setConfigInternal] = useState<Config>({
|
|
||||||
name: 'default',
|
|
||||||
apps: [],
|
|
||||||
settings: {
|
|
||||||
searchUrl: 'https://www.google.com/search?q=',
|
|
||||||
},
|
|
||||||
modules: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadConfig(configName: string) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`/api/configs/${configName}`);
|
|
||||||
setConfigInternal(JSON.parse(response.data));
|
|
||||||
showNotification({
|
|
||||||
title: 'Config',
|
|
||||||
icon: <Check />,
|
|
||||||
color: 'green',
|
|
||||||
autoClose: 1500,
|
|
||||||
radius: 'md',
|
|
||||||
message: `Loaded config : ${configName}`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
showNotification({
|
|
||||||
title: 'Config',
|
|
||||||
icon: <X />,
|
|
||||||
color: 'red',
|
|
||||||
autoClose: 1500,
|
|
||||||
radius: 'md',
|
|
||||||
message: `Error loading config : ${configName}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setConfig(newconfig: Config) {
|
|
||||||
axios.put(`/api/configs/${newconfig.name}`, newconfig);
|
|
||||||
setConfigInternal(newconfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getConfigs(): Promise<string[]> {
|
|
||||||
const response = await axios.get('/api/configs');
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
config,
|
|
||||||
setConfig,
|
|
||||||
loadConfig,
|
|
||||||
getConfigs,
|
|
||||||
};
|
|
||||||
return <configContext.Provider value={value}>{children}</configContext.Provider>;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user