mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 17:26:26 +01:00
@@ -4,8 +4,8 @@
|
|||||||
"description": "Embed a video stream or video from a camera or a website",
|
"description": "Embed a video stream or video from a camera or a website",
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings for video stream widget",
|
"title": "Settings for video stream widget",
|
||||||
"cameraFeedUrl": {
|
"FeedUrl": {
|
||||||
"label": "Camera feed url"
|
"label": "Feed url"
|
||||||
},
|
},
|
||||||
"autoPlay": {
|
"autoPlay": {
|
||||||
"label": "Auto play"
|
"label": "Auto play"
|
||||||
|
|||||||
274
src/widgets/download-speed/Tile.tsx
Normal file
274
src/widgets/download-speed/Tile.tsx
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useElementSize, useListState } from '@mantine/hooks';
|
||||||
|
import { linearGradientDef } from '@nivo/core';
|
||||||
|
import { Serie, Datum, ResponsiveLine } from '@nivo/line';
|
||||||
|
import { IconDownload, IconUpload } from '@tabler/icons';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useConfigContext } from '../../config/provider';
|
||||||
|
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
|
||||||
|
import { useColorTheme } from '../../tools/color';
|
||||||
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
|
import {
|
||||||
|
NormalizedDownloadQueueResponse,
|
||||||
|
TorrentTotalDownload,
|
||||||
|
} from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
|
import definition, { ITorrentNetworkTraffic } from './TorrentNetworkTrafficTile';
|
||||||
|
|
||||||
|
interface TorrentNetworkTrafficTileProps {
|
||||||
|
widget: ITorrentNetworkTraffic;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTrafficTileProps) {
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
const { ref: refRoot, height: heightRoot } = useElementSize();
|
||||||
|
const { ref: refTitle, height: heightTitle } = useElementSize();
|
||||||
|
const { ref: refFooter, height: heightFooter } = useElementSize();
|
||||||
|
const { primaryColor, secondaryColor } = useColorTheme();
|
||||||
|
const { t } = useTranslation(`modules/${definition.id}`);
|
||||||
|
|
||||||
|
const [clientDataHistory, setClientDataHistory] = useListState<NormalizedDownloadQueueResponse>();
|
||||||
|
|
||||||
|
const { data, dataUpdatedAt } = useGetDownloadClientsQueue();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setClientDataHistory.append(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientDataHistory.length < 30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setClientDataHistory.remove(0);
|
||||||
|
}, [dataUpdatedAt]);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recoredAppsOverTime = clientDataHistory.flatMap((x) => x.apps.map((app) => app));
|
||||||
|
|
||||||
|
// removing duplicates the "naive" way: https://stackoverflow.com/a/9229821/15257712
|
||||||
|
const uniqueRecordedAppsOverTime = recoredAppsOverTime
|
||||||
|
.map((x) => x.appId)
|
||||||
|
.filter((item, position) => recoredAppsOverTime.map((y) => y.appId).indexOf(item) === position);
|
||||||
|
|
||||||
|
const lineChartData: Serie[] = uniqueRecordedAppsOverTime.flatMap((appId) => {
|
||||||
|
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||||
|
|
||||||
|
const series: Serie[] = [
|
||||||
|
{
|
||||||
|
id: `download_${appId}`,
|
||||||
|
data: records.map((record, index) => ({
|
||||||
|
x: index,
|
||||||
|
y: record.totalDownload,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (records.some((x) => x.type === 'torrent')) {
|
||||||
|
const torrentRecords = records.map((record, index): Datum | null => {
|
||||||
|
if (record.type !== 'torrent') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: index,
|
||||||
|
y: record.totalUpload,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const filteredRecords = torrentRecords.filter((x) => x !== null) as Datum[];
|
||||||
|
series.push({
|
||||||
|
id: `upload_${appId}`,
|
||||||
|
data: filteredRecords,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalDownload = uniqueRecordedAppsOverTime
|
||||||
|
.map((appId) => {
|
||||||
|
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||||
|
const lastRecord = records.at(-1);
|
||||||
|
return lastRecord?.totalDownload ?? 0;
|
||||||
|
})
|
||||||
|
.reduce((acc, n) => acc + n, 0);
|
||||||
|
|
||||||
|
const totalUpload = uniqueRecordedAppsOverTime
|
||||||
|
.map((appId) => {
|
||||||
|
const records = recoredAppsOverTime.filter((x) => x.appId === appId && x.type === 'torrent');
|
||||||
|
const lastRecord = records.at(-1) as TorrentTotalDownload;
|
||||||
|
return lastRecord?.totalUpload ?? 0;
|
||||||
|
})
|
||||||
|
.reduce((acc, n) => acc + n, 0);
|
||||||
|
|
||||||
|
const graphHeight = heightRoot - heightFooter - heightTitle;
|
||||||
|
|
||||||
|
const { colors } = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack ref={refRoot} style={{ height: '100%' }}>
|
||||||
|
<Group ref={refTitle}>
|
||||||
|
<IconDownload />
|
||||||
|
<Title order={4}>{t('card.lineChart.title')}</Title>
|
||||||
|
</Group>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
height: graphHeight,
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box style={{ height: '100%', width: '100%', position: 'absolute' }}>
|
||||||
|
<ResponsiveLine
|
||||||
|
isInteractive
|
||||||
|
enableSlices="x"
|
||||||
|
sliceTooltip={({ slice }) => {
|
||||||
|
const { points } = slice;
|
||||||
|
|
||||||
|
const recordsFromPoints = uniqueRecordedAppsOverTime.map((appId) => {
|
||||||
|
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||||
|
const point = points.find((x) => x.id.includes(appId));
|
||||||
|
const pointIndex = Number(point?.data.x) ?? 0;
|
||||||
|
const color = point?.serieColor;
|
||||||
|
return {
|
||||||
|
record: records[pointIndex],
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card p="xs" radius="md" withBorder>
|
||||||
|
<Card.Section p="xs">
|
||||||
|
<Stack spacing="xs">
|
||||||
|
{recordsFromPoints.map((entry, index) => {
|
||||||
|
const app = config?.apps.find((x) => x.id === entry.record.appId);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group key={`download-client-tooltip-${index}`}>
|
||||||
|
<AppAvatar iconUrl={app.appearance.iconUrl} />
|
||||||
|
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Text size="sm">{app.name}</Text>
|
||||||
|
<Group>
|
||||||
|
<Group spacing="xs">
|
||||||
|
<IconDownload opacity={0.6} size={14} />
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{humanFileSize(entry.record.totalDownload, false)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{entry.record.type === 'torrent' && (
|
||||||
|
<Group spacing="xs">
|
||||||
|
<IconUpload opacity={0.6} size={14} />
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{humanFileSize(entry.record.totalUpload, false)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Card.Section>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
data={lineChartData}
|
||||||
|
curve="monotoneX"
|
||||||
|
yFormat=" >-.2f"
|
||||||
|
axisLeft={null}
|
||||||
|
axisBottom={null}
|
||||||
|
axisRight={null}
|
||||||
|
enablePoints={false}
|
||||||
|
enableGridX={false}
|
||||||
|
enableGridY={false}
|
||||||
|
enableArea
|
||||||
|
defs={[
|
||||||
|
linearGradientDef('gradientA', [
|
||||||
|
{ offset: 0, color: 'inherit' },
|
||||||
|
{ offset: 100, color: 'inherit', opacity: 0 },
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
colors={lineChartData.flatMap((data) =>
|
||||||
|
data.id.toString().startsWith('upload_')
|
||||||
|
? colors[secondaryColor][5]
|
||||||
|
: colors[primaryColor][5]
|
||||||
|
)}
|
||||||
|
fill={[{ match: '*', id: 'gradientA' }]}
|
||||||
|
margin={{ bottom: 5 }}
|
||||||
|
animate={false}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group position="apart" ref={refFooter}>
|
||||||
|
<Group>
|
||||||
|
<Group spacing="xs">
|
||||||
|
<IconDownload color={colors[primaryColor][5]} opacity={0.6} size={18} />
|
||||||
|
<Text color="dimmed" size="sm">
|
||||||
|
{humanFileSize(totalDownload, false)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group spacing="xs">
|
||||||
|
<IconUpload color={colors[secondaryColor][5]} opacity={0.6} size={18} />
|
||||||
|
<Text color="dimmed" size="sm">
|
||||||
|
{humanFileSize(totalUpload, false)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Avatar.Group>
|
||||||
|
{uniqueRecordedAppsOverTime.map((appId, index) => {
|
||||||
|
const app = config?.apps.find((x) => x.id === appId);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={app.name}
|
||||||
|
key={`download-client-app-tooltip-${index}`}
|
||||||
|
withArrow
|
||||||
|
withinPortal
|
||||||
|
>
|
||||||
|
<AppAvatar iconUrl={app.appearance.iconUrl} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Avatar.Group>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppAvatar = ({ iconUrl }: { iconUrl: string }) => {
|
||||||
|
const { colors, colorScheme } = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
src={iconUrl}
|
||||||
|
bg={colorScheme === 'dark' ? colors.gray[8] : colors.gray[2]}
|
||||||
|
size="sm"
|
||||||
|
radius="xl"
|
||||||
|
p={4}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,31 +1,13 @@
|
|||||||
import {
|
import { IconArrowsUpDown } from '@tabler/icons';
|
||||||
Avatar,
|
|
||||||
Box,
|
import dynamic from 'next/dynamic';
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useElementSize, useListState } from '@mantine/hooks';
|
|
||||||
import { linearGradientDef } from '@nivo/core';
|
|
||||||
import { Datum, ResponsiveLine, Serie } from '@nivo/line';
|
|
||||||
import { IconArrowsUpDown, IconDownload, IconUpload } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useConfigContext } from '../../config/provider';
|
|
||||||
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
|
|
||||||
import { useColorTheme } from '../../tools/color';
|
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
|
||||||
import {
|
|
||||||
NormalizedDownloadQueueResponse,
|
|
||||||
TorrentTotalDownload,
|
|
||||||
} from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
|
|
||||||
|
const torrentNetworkTrafficTile = dynamic(() => import('./Tile'), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
id: 'dlspeed',
|
id: 'dlspeed',
|
||||||
icon: IconArrowsUpDown,
|
icon: IconArrowsUpDown,
|
||||||
@@ -37,257 +19,9 @@ const definition = defineWidget({
|
|||||||
maxWidth: 12,
|
maxWidth: 12,
|
||||||
maxHeight: 6,
|
maxHeight: 6,
|
||||||
},
|
},
|
||||||
component: TorrentNetworkTrafficTile,
|
component: torrentNetworkTrafficTile,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ITorrentNetworkTraffic = IWidget<(typeof definition)['id'], typeof definition>;
|
export type ITorrentNetworkTraffic = IWidget<(typeof definition)['id'], typeof definition>;
|
||||||
|
|
||||||
interface TorrentNetworkTrafficTileProps {
|
|
||||||
widget: ITorrentNetworkTraffic;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTrafficTileProps) {
|
|
||||||
const { config } = useConfigContext();
|
|
||||||
const { ref: refRoot, height: heightRoot } = useElementSize();
|
|
||||||
const { ref: refTitle, height: heightTitle } = useElementSize();
|
|
||||||
const { ref: refFooter, height: heightFooter } = useElementSize();
|
|
||||||
const { primaryColor, secondaryColor } = useColorTheme();
|
|
||||||
const { t } = useTranslation(`modules/${definition.id}`);
|
|
||||||
|
|
||||||
const [clientDataHistory, setClientDataHistory] = useListState<NormalizedDownloadQueueResponse>();
|
|
||||||
|
|
||||||
const { data, dataUpdatedAt } = useGetDownloadClientsQueue();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setClientDataHistory.append(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientDataHistory.length < 30) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setClientDataHistory.remove(0);
|
|
||||||
}, [dataUpdatedAt]);
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recoredAppsOverTime = clientDataHistory.flatMap((x) => x.apps.map((app) => app));
|
|
||||||
|
|
||||||
// removing duplicates the "naive" way: https://stackoverflow.com/a/9229821/15257712
|
|
||||||
const uniqueRecordedAppsOverTime = recoredAppsOverTime
|
|
||||||
.map((x) => x.appId)
|
|
||||||
.filter((item, position) => recoredAppsOverTime.map((y) => y.appId).indexOf(item) === position);
|
|
||||||
|
|
||||||
const lineChartData: Serie[] = uniqueRecordedAppsOverTime.flatMap((appId) => {
|
|
||||||
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
|
||||||
|
|
||||||
const series: Serie[] = [
|
|
||||||
{
|
|
||||||
id: `download_${appId}`,
|
|
||||||
data: records.map((record, index) => ({
|
|
||||||
x: index,
|
|
||||||
y: record.totalDownload,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (records.some((x) => x.type === 'torrent')) {
|
|
||||||
const torrentRecords = records.map((record, index): Datum | null => {
|
|
||||||
if (record.type !== 'torrent') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: index,
|
|
||||||
y: record.totalUpload,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const filteredRecords = torrentRecords.filter((x) => x !== null) as Datum[];
|
|
||||||
series.push({
|
|
||||||
id: `upload_${appId}`,
|
|
||||||
data: filteredRecords,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return series;
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalDownload = uniqueRecordedAppsOverTime
|
|
||||||
.map((appId) => {
|
|
||||||
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
|
||||||
const lastRecord = records.at(-1);
|
|
||||||
return lastRecord?.totalDownload ?? 0;
|
|
||||||
})
|
|
||||||
.reduce((acc, n) => acc + n, 0);
|
|
||||||
|
|
||||||
const totalUpload = uniqueRecordedAppsOverTime
|
|
||||||
.map((appId) => {
|
|
||||||
const records = recoredAppsOverTime.filter((x) => x.appId === appId && x.type === 'torrent');
|
|
||||||
const lastRecord = records.at(-1) as TorrentTotalDownload;
|
|
||||||
return lastRecord?.totalUpload ?? 0;
|
|
||||||
})
|
|
||||||
.reduce((acc, n) => acc + n, 0);
|
|
||||||
|
|
||||||
const graphHeight = heightRoot - heightFooter - heightTitle;
|
|
||||||
|
|
||||||
const { colors } = useMantineTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack ref={refRoot} style={{ height: '100%' }}>
|
|
||||||
<Group ref={refTitle}>
|
|
||||||
<IconDownload />
|
|
||||||
<Title order={4}>{t('card.lineChart.title')}</Title>
|
|
||||||
</Group>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
height: graphHeight,
|
|
||||||
width: '100%',
|
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box style={{ height: '100%', width: '100%', position: 'absolute' }}>
|
|
||||||
<ResponsiveLine
|
|
||||||
isInteractive
|
|
||||||
enableSlices="x"
|
|
||||||
sliceTooltip={({ slice }) => {
|
|
||||||
const { points } = slice;
|
|
||||||
|
|
||||||
const recordsFromPoints = uniqueRecordedAppsOverTime.map((appId) => {
|
|
||||||
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
|
||||||
const point = points.find((x) => x.id.includes(appId));
|
|
||||||
const pointIndex = Number(point?.data.x) ?? 0;
|
|
||||||
const color = point?.serieColor;
|
|
||||||
return {
|
|
||||||
record: records[pointIndex],
|
|
||||||
color,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card p="xs" radius="md" withBorder>
|
|
||||||
<Card.Section p="xs">
|
|
||||||
<Stack spacing="xs">
|
|
||||||
{recordsFromPoints.map((entry, index) => {
|
|
||||||
const app = config?.apps.find((x) => x.id === entry.record.appId);
|
|
||||||
|
|
||||||
if (!app) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group key={`download-client-tooltip-${index}`}>
|
|
||||||
<AppAvatar iconUrl={app.appearance.iconUrl} />
|
|
||||||
|
|
||||||
<Stack spacing={0}>
|
|
||||||
<Text size="sm">{app.name}</Text>
|
|
||||||
<Group>
|
|
||||||
<Group spacing="xs">
|
|
||||||
<IconDownload opacity={0.6} size={14} />
|
|
||||||
<Text size="xs" color="dimmed">
|
|
||||||
{humanFileSize(entry.record.totalDownload, false)}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{entry.record.type === 'torrent' && (
|
|
||||||
<Group spacing="xs">
|
|
||||||
<IconUpload opacity={0.6} size={14} />
|
|
||||||
<Text size="xs" color="dimmed">
|
|
||||||
{humanFileSize(entry.record.totalUpload, false)}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
</Card.Section>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
data={lineChartData}
|
|
||||||
curve="monotoneX"
|
|
||||||
yFormat=" >-.2f"
|
|
||||||
axisLeft={null}
|
|
||||||
axisBottom={null}
|
|
||||||
axisRight={null}
|
|
||||||
enablePoints={false}
|
|
||||||
enableGridX={false}
|
|
||||||
enableGridY={false}
|
|
||||||
enableArea
|
|
||||||
defs={[
|
|
||||||
linearGradientDef('gradientA', [
|
|
||||||
{ offset: 0, color: 'inherit' },
|
|
||||||
{ offset: 100, color: 'inherit', opacity: 0 },
|
|
||||||
]),
|
|
||||||
]}
|
|
||||||
colors={lineChartData.flatMap((data) =>
|
|
||||||
data.id.toString().startsWith('upload_')
|
|
||||||
? colors[secondaryColor][5]
|
|
||||||
: colors[primaryColor][5]
|
|
||||||
)}
|
|
||||||
fill={[{ match: '*', id: 'gradientA' }]}
|
|
||||||
margin={{ bottom: 5 }}
|
|
||||||
animate={false}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Group position="apart" ref={refFooter}>
|
|
||||||
<Group>
|
|
||||||
<Group spacing="xs">
|
|
||||||
<IconDownload color={colors[primaryColor][5]} opacity={0.6} size={18} />
|
|
||||||
<Text color="dimmed" size="sm">
|
|
||||||
{humanFileSize(totalDownload, false)}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group spacing="xs">
|
|
||||||
<IconUpload color={colors[secondaryColor][5]} opacity={0.6} size={18} />
|
|
||||||
<Text color="dimmed" size="sm">
|
|
||||||
{humanFileSize(totalUpload, false)}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Avatar.Group>
|
|
||||||
{uniqueRecordedAppsOverTime.map((appId, index) => {
|
|
||||||
const app = config?.apps.find((x) => x.id === appId);
|
|
||||||
|
|
||||||
if (!app) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
label={app.name}
|
|
||||||
key={`download-client-app-tooltip-${index}`}
|
|
||||||
withArrow
|
|
||||||
withinPortal
|
|
||||||
>
|
|
||||||
<AppAvatar iconUrl={app.appearance.iconUrl} />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Avatar.Group>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppAvatar = ({ iconUrl }: { iconUrl: string }) => {
|
|
||||||
const { colors, colorScheme } = useMantineTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
src={iconUrl}
|
|
||||||
bg={colorScheme === 'dark' ? colors.gray[8] : colors.gray[2]}
|
|
||||||
size="sm"
|
|
||||||
radius="xl"
|
|
||||||
p={4}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default definition;
|
export default definition;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Center, Group, Stack, Title } from '@mantine/core';
|
import { Center, Group, Stack, Title } from '@mantine/core';
|
||||||
import { IconDeviceCctv, IconHeartBroken } from '@tabler/icons';
|
import { IconDeviceCctv, IconHeartBroken } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import VideoFeed from './VideoFeed';
|
|
||||||
|
const VideoFeed = dynamic(() => import('./VideoFeed'), { ssr: false });
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
id: 'video-stream',
|
id: 'video-stream',
|
||||||
icon: IconDeviceCctv,
|
icon: IconDeviceCctv,
|
||||||
options: {
|
options: {
|
||||||
cameraFeedUrl: {
|
FeedUrl: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
},
|
},
|
||||||
@@ -43,7 +45,7 @@ interface VideoStreamWidgetProps {
|
|||||||
|
|
||||||
function VideoStreamWidget({ widget }: VideoStreamWidgetProps) {
|
function VideoStreamWidget({ widget }: VideoStreamWidgetProps) {
|
||||||
const { t } = useTranslation('modules/video-stream');
|
const { t } = useTranslation('modules/video-stream');
|
||||||
if (!widget.properties.cameraFeedUrl) {
|
if (!widget.properties.FeedUrl) {
|
||||||
return (
|
return (
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<Stack align="center">
|
<Stack align="center">
|
||||||
@@ -56,7 +58,7 @@ function VideoStreamWidget({ widget }: VideoStreamWidgetProps) {
|
|||||||
return (
|
return (
|
||||||
<Group position="center" w="100%" h="100%">
|
<Group position="center" w="100%" h="100%">
|
||||||
<VideoFeed
|
<VideoFeed
|
||||||
source={widget?.properties.cameraFeedUrl}
|
source={widget?.properties.FeedUrl}
|
||||||
muted={widget?.properties.muted}
|
muted={widget?.properties.muted}
|
||||||
autoPlay={widget?.properties.autoPlay}
|
autoPlay={widget?.properties.autoPlay}
|
||||||
controls={widget?.properties.controls}
|
controls={widget?.properties.controls}
|
||||||
|
|||||||
Reference in New Issue
Block a user