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-use-before-define': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'linebreak-style': 0
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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 { 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 { useConfig } from '../../tools/state';
|
||||
import { useConfigStore } from '../../config/store';
|
||||
import { Config } from '../../tools/types';
|
||||
import { migrateToIdConfig } from '../../tools/migrate';
|
||||
|
||||
export default function LoadConfigComponent(props: any) {
|
||||
const { setConfig } = useConfig();
|
||||
const { updateConfig } = useConfigStore();
|
||||
const theme = useMantineTheme();
|
||||
const { t } = useTranslation('settings/general/config-changer');
|
||||
|
||||
@@ -48,8 +47,7 @@ export default function LoadConfigComponent(props: any) {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
const migratedConfig = migrateToIdConfig(newConfig);
|
||||
setConfig(migratedConfig);
|
||||
updateConfig(newConfig.name, (previousConfig) => ({ ...previousConfig, newConfig }));
|
||||
});
|
||||
}}
|
||||
accept={['application/json']}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import { ReactNode, RefObject } from 'react';
|
||||
|
||||
interface GridstackTileWrapperProps {
|
||||
|
||||
@@ -6,10 +6,10 @@ import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { CategoryType } from '../../../../types/category';
|
||||
|
||||
export interface CategoryEditModalInnerProps {
|
||||
export type CategoryEditModalInnerProps = {
|
||||
category: CategoryType;
|
||||
onSuccess: (category: CategoryType) => Promise<void>;
|
||||
}
|
||||
};
|
||||
|
||||
export const CategoryEditModal = ({
|
||||
context,
|
||||
|
||||
@@ -25,6 +25,7 @@ export const DashboardSidebar = ({ location }: DashboardSidebarProps) => {
|
||||
className="grid-stack grid-stack-sidebar"
|
||||
style={{ transitionDuration: '0s', height: '100%' }}
|
||||
data-sidebar={location}
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
gs-min-row={minRow}
|
||||
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
|
||||
// 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
|
||||
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(
|
||||
(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 { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
import { RequestModal } from '../overseerr/RequestModal';
|
||||
import { Result } from '../overseerr/SearchResult';
|
||||
|
||||
@@ -27,9 +26,15 @@ export interface IMedia {
|
||||
|
||||
export function OverseerrMediaDisplay(props: any) {
|
||||
const { media }: { media: Result } = props;
|
||||
const { config } = useConfig();
|
||||
const { config } = useConfigContext();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const service = config.apps.find(
|
||||
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
||||
(service) =>
|
||||
service.integration.type === 'overseerr' || service.integration.type === 'jellyseerr'
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -45,9 +50,9 @@ export function OverseerrMediaDisplay(props: any) {
|
||||
plexUrl: media.mediaInfo?.plexUrl ?? media.mediaInfo?.mediaUrl,
|
||||
voteAverage: media.voteAverage?.toString(),
|
||||
overseerrResult: media,
|
||||
overseerrId: `${service?.openedUrl ? service?.openedUrl : service?.url}/${
|
||||
media.mediaType
|
||||
}/${media.id}`,
|
||||
overseerrId: `${
|
||||
service?.behaviour.externalUrl ? service.behaviour.externalUrl : service?.url
|
||||
}/${media.mediaType}/${media.id}`,
|
||||
type: 'overseer',
|
||||
}}
|
||||
/>
|
||||
@@ -56,16 +61,21 @@ export function OverseerrMediaDisplay(props: any) {
|
||||
|
||||
export function ReadarrMediaDisplay(props: any) {
|
||||
const { media }: { media: any } = props;
|
||||
const { config } = useConfig();
|
||||
const { config } = useConfigContext();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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
|
||||
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
||||
if (!readarr) {
|
||||
return null;
|
||||
}
|
||||
const baseUrl = readarr.openedUrl
|
||||
? new URL(readarr.openedUrl).origin
|
||||
const baseUrl = readarr.behaviour.externalUrl
|
||||
? new URL(readarr.behaviour.externalUrl).origin
|
||||
: new URL(readarr.url).origin;
|
||||
// Remove '/' from the end of the lidarr url
|
||||
const fullLink = poster ? `${baseUrl}${poster.url}` : undefined;
|
||||
@@ -88,15 +98,22 @@ export function ReadarrMediaDisplay(props: any) {
|
||||
|
||||
export function LidarrMediaDisplay(props: any) {
|
||||
const { media }: { media: any } = props;
|
||||
const { config } = useConfig();
|
||||
const { config } = useConfigContext();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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
|
||||
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
||||
if (!lidarr) {
|
||||
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
|
||||
const fullLink = poster ? `${baseUrl}${poster.url}` : undefined;
|
||||
// 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 */
|
||||
import { Button, Group } from '@mantine/core';
|
||||
import { closeModal, openModal } from '@mantine/modals';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconCheck,
|
||||
@@ -17,9 +16,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { TFunction } from 'react-i18next';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { tryMatchService } from '../../tools/addToHomarr';
|
||||
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { AppType } from '../../types/app';
|
||||
|
||||
let t: TFunction<'modules/docker', undefined>;
|
||||
@@ -71,7 +68,6 @@ export interface ContainerActionBarProps {
|
||||
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
|
||||
t = useTranslation('modules/docker').t;
|
||||
const [isLoading, setisLoading] = useState(false);
|
||||
const { config, setConfig } = useConfig();
|
||||
|
||||
return (
|
||||
<Group spacing="xs">
|
||||
@@ -182,7 +178,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
externalUrl: '',
|
||||
},
|
||||
area: {
|
||||
type: 'wrapper', // TODO: Set the wrapper automatically
|
||||
type: 'sidebar', // TODO: Set the wrapper automatically
|
||||
properties: {
|
||||
location: 'right',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
location: {
|
||||
@@ -194,6 +193,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
integration: {
|
||||
type: null,
|
||||
properties: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
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 { IconBrandDocker, IconX } from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import Docker from 'dockerode';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
import ContainerActionBar from './ContainerActionBar';
|
||||
import DockerTable from './DockerTable';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
|
||||
export const DockerModule: IModule = {
|
||||
title: 'Docker',
|
||||
@@ -22,17 +22,18 @@ export default function DockerMenuButton(props: any) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [containers, setContainers] = useState<Docker.ContainerInfo[]>([]);
|
||||
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
|
||||
const { config } = useConfig();
|
||||
const moduleEnabled = config.modules?.[DockerModule.id]?.enabled ?? false;
|
||||
const { config } = useConfigContext();
|
||||
|
||||
const dockerEnabled = config?.settings.customization.layout.enabledDocker || false;
|
||||
|
||||
const { t } = useTranslation('modules/docker');
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [config.modules]);
|
||||
}, [config?.settings]);
|
||||
|
||||
function reload() {
|
||||
if (!moduleEnabled) {
|
||||
if (!dockerEnabled) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
@@ -56,8 +57,8 @@ export default function DockerMenuButton(props: any) {
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
const exists = config.modules?.[DockerModule.id]?.enabled ?? false;
|
||||
if (!exists) {
|
||||
|
||||
if (!dockerEnabled) {
|
||||
return null;
|
||||
}
|
||||
// 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 './weather';
|
||||
export * from './docker';
|
||||
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 { IconPlug as Plug } from '@tabler/icons';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IconPlug as Plug } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
|
||||
export const PingModule: IModule = {
|
||||
@@ -16,12 +16,12 @@ export const PingModule: IModule = {
|
||||
|
||||
export default function PingComponent(props: any) {
|
||||
type State = 'loading' | 'down' | 'online';
|
||||
const { config } = useConfig();
|
||||
const { config } = useConfigContext();
|
||||
|
||||
const { url }: { url: string } = props;
|
||||
const [isOnline, setOnline] = useState<State>('loading');
|
||||
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');
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function PingComponent(props: any) {
|
||||
.catch((error) => {
|
||||
statusCheck(error.response);
|
||||
});
|
||||
}, [config.modules?.[PingModule.id]?.enabled]);
|
||||
}, [config?.settings.customization.layout.enabledPing]);
|
||||
if (!exists) {
|
||||
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 path from 'path';
|
||||
import { useEffect } from 'react';
|
||||
import AppShelf from '../components/AppShelf/AppShelf';
|
||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||
import { Dashboard } from '../components/Dashboard/Dashboard';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { useConfigContext } from '../config/provider';
|
||||
import { useConfigStore } from '../config/store';
|
||||
import { getConfig } from '../tools/getConfig';
|
||||
import { useConfig } from '../tools/state';
|
||||
import { dashboardNamespaces } from '../tools/translation-namespaces';
|
||||
import { Config } from '../tools/types';
|
||||
import { ConfigType } from '../types/config';
|
||||
|
||||
type ServerSideProps = {
|
||||
config: ConfigType;
|
||||
};
|
||||
|
||||
export async function getServerSideProps({
|
||||
req,
|
||||
res,
|
||||
locale,
|
||||
query,
|
||||
}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> {
|
||||
}: GetServerSidePropsContext): Promise<{ props: ServerSideProps }> {
|
||||
const configByUrl = query.slug;
|
||||
const configPath = path.join(process.cwd(), 'data/configs', `${configByUrl}.json`);
|
||||
const configExists = fs.existsSync(configPath);
|
||||
@@ -28,12 +33,36 @@ export async function getServerSideProps({
|
||||
return {
|
||||
props: {
|
||||
config: {
|
||||
name: 'Default config',
|
||||
schemaVersion: '1.0',
|
||||
configProperties: {
|
||||
name: 'Default Configuration',
|
||||
},
|
||||
apps: [],
|
||||
settings: {
|
||||
searchUrl: 'https://www.google.com/search?q=',
|
||||
common: {
|
||||
searchEngine: {
|
||||
type: 'google',
|
||||
properties: {
|
||||
enabled: true,
|
||||
openInNewTab: true,
|
||||
},
|
||||
modules: {},
|
||||
},
|
||||
defaultConfig: 'default',
|
||||
},
|
||||
customization: {
|
||||
layout: {
|
||||
enabledLeftSidebar: false,
|
||||
enabledRightSidebar: false,
|
||||
enabledSearchbar: true,
|
||||
enabledDocker: false,
|
||||
enabledPing: false,
|
||||
},
|
||||
colors: {},
|
||||
},
|
||||
},
|
||||
categories: [],
|
||||
wrappers: [],
|
||||
widgets: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -46,14 +75,18 @@ export async function getServerSideProps({
|
||||
}
|
||||
|
||||
export default function HomePage(props: any) {
|
||||
const { config: initialConfig }: { config: Config } = props;
|
||||
const { setConfig } = useConfig();
|
||||
const { config: initialConfig }: { config: ConfigType } = props;
|
||||
const { name: configName } = useConfigContext();
|
||||
const { updateConfig } = useConfigStore();
|
||||
useEffect(() => {
|
||||
setConfig(initialConfig);
|
||||
if (!configName) {
|
||||
return;
|
||||
}
|
||||
updateConfig(configName, () => initialConfig);
|
||||
}, [initialConfig]);
|
||||
return (
|
||||
<Layout>
|
||||
<AppShelf />
|
||||
<Dashboard />
|
||||
<LoadConfigComponent />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { SSRConfig } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||
@@ -12,7 +13,9 @@ import { ConfigType } from '../types/config';
|
||||
|
||||
type ServerSideProps = {
|
||||
config: ConfigType;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
configName: string;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
_nextI18Next: SSRConfig['_nextI18Next'];
|
||||
};
|
||||
|
||||
|
||||
@@ -35,23 +35,3 @@ export function tryMatchService(container: Dockerode.ContainerInfo | undefined)
|
||||
.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',
|
||||
},
|
||||
categories: [],
|
||||
widgets: {},
|
||||
widgets: [],
|
||||
apps: [],
|
||||
settings: {
|
||||
common: {
|
||||
|
||||
@@ -6,18 +6,17 @@ export const getFrontendConfig = (name: string): ConfigType => {
|
||||
|
||||
return {
|
||||
...config,
|
||||
apps: config.apps.map((s) => ({
|
||||
...s,
|
||||
integration: s.integration
|
||||
? {
|
||||
...s.integration,
|
||||
properties: s.integration?.properties.map((p) => ({
|
||||
...p,
|
||||
value: p.type === 'private' ? null : p.value,
|
||||
isDefined: p.value != null,
|
||||
})),
|
||||
}
|
||||
: null,
|
||||
apps: config.apps.map((app) => ({
|
||||
...app,
|
||||
integration: {
|
||||
...app.integration ?? null,
|
||||
type: app.integration?.type ?? null,
|
||||
properties: app.integration?.properties.map((property) => ({
|
||||
...property,
|
||||
value: property.type === 'private' ? undefined : property.value,
|
||||
isDefined: property.value != 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