diff --git a/public/locales/en/modules/video-stream.json b/public/locales/en/modules/video-stream.json index 3b84c3574..a35c740a3 100644 --- a/public/locales/en/modules/video-stream.json +++ b/public/locales/en/modules/video-stream.json @@ -4,8 +4,8 @@ "description": "Embed a video stream or video from a camera or a website", "settings": { "title": "Settings for video stream widget", - "cameraFeedUrl": { - "label": "Camera feed url" + "FeedUrl": { + "label": "Feed url" }, "autoPlay": { "label": "Auto play" diff --git a/src/widgets/download-speed/Tile.tsx b/src/widgets/download-speed/Tile.tsx new file mode 100644 index 000000000..1926b1c8d --- /dev/null +++ b/src/widgets/download-speed/Tile.tsx @@ -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(); + + 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 ( + + + + {t('card.lineChart.title')} + + + + { + 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 ( + + + + {recordsFromPoints.map((entry, index) => { + const app = config?.apps.find((x) => x.id === entry.record.appId); + + if (!app) { + return null; + } + + return ( + + + + + {app.name} + + + + + {humanFileSize(entry.record.totalDownload, false)} + + + + {entry.record.type === 'torrent' && ( + + + + {humanFileSize(entry.record.totalUpload, false)} + + + )} + + + + ); + })} + + + + ); + }} + 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} + /> + + + + + + + + + {humanFileSize(totalDownload, false)} + + + + + + {humanFileSize(totalUpload, false)} + + + + + {uniqueRecordedAppsOverTime.map((appId, index) => { + const app = config?.apps.find((x) => x.id === appId); + + if (!app) { + return null; + } + + return ( + + + + ); + })} + + + + ); +} + +const AppAvatar = ({ iconUrl }: { iconUrl: string }) => { + const { colors, colorScheme } = useMantineTheme(); + + return ( + + ); +}; diff --git a/src/widgets/download-speed/TorrentNetworkTrafficTile.tsx b/src/widgets/download-speed/TorrentNetworkTrafficTile.tsx index 4a0feff0a..85d579a74 100644 --- a/src/widgets/download-speed/TorrentNetworkTrafficTile.tsx +++ b/src/widgets/download-speed/TorrentNetworkTrafficTile.tsx @@ -1,31 +1,13 @@ -import { - Avatar, - Box, - 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 { IconArrowsUpDown } from '@tabler/icons'; + +import dynamic from 'next/dynamic'; import { defineWidget } from '../helper'; import { IWidget } from '../widgets'; +const torrentNetworkTrafficTile = dynamic(() => import('./Tile'), { + ssr: false, +}); + const definition = defineWidget({ id: 'dlspeed', icon: IconArrowsUpDown, @@ -37,257 +19,9 @@ const definition = defineWidget({ maxWidth: 12, maxHeight: 6, }, - component: TorrentNetworkTrafficTile, + component: torrentNetworkTrafficTile, }); 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(); - - 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 ( - - - - {t('card.lineChart.title')} - - - - { - 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 ( - - - - {recordsFromPoints.map((entry, index) => { - const app = config?.apps.find((x) => x.id === entry.record.appId); - - if (!app) { - return null; - } - - return ( - - - - - {app.name} - - - - - {humanFileSize(entry.record.totalDownload, false)} - - - - {entry.record.type === 'torrent' && ( - - - - {humanFileSize(entry.record.totalUpload, false)} - - - )} - - - - ); - })} - - - - ); - }} - 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} - /> - - - - - - - - - {humanFileSize(totalDownload, false)} - - - - - - {humanFileSize(totalUpload, false)} - - - - - {uniqueRecordedAppsOverTime.map((appId, index) => { - const app = config?.apps.find((x) => x.id === appId); - - if (!app) { - return null; - } - - return ( - - - - ); - })} - - - - ); -} - -const AppAvatar = ({ iconUrl }: { iconUrl: string }) => { - const { colors, colorScheme } = useMantineTheme(); - - return ( - - ); -}; - export default definition; diff --git a/src/widgets/video/VideoStreamTile.tsx b/src/widgets/video/VideoStreamTile.tsx index 028809f99..7db8a2c80 100644 --- a/src/widgets/video/VideoStreamTile.tsx +++ b/src/widgets/video/VideoStreamTile.tsx @@ -1,15 +1,17 @@ import { Center, Group, Stack, Title } from '@mantine/core'; import { IconDeviceCctv, IconHeartBroken } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; +import dynamic from 'next/dynamic'; import { defineWidget } from '../helper'; import { IWidget } from '../widgets'; -import VideoFeed from './VideoFeed'; + +const VideoFeed = dynamic(() => import('./VideoFeed'), { ssr: false }); const definition = defineWidget({ id: 'video-stream', icon: IconDeviceCctv, options: { - cameraFeedUrl: { + FeedUrl: { type: 'text', defaultValue: '', }, @@ -43,7 +45,7 @@ interface VideoStreamWidgetProps { function VideoStreamWidget({ widget }: VideoStreamWidgetProps) { const { t } = useTranslation('modules/video-stream'); - if (!widget.properties.cameraFeedUrl) { + if (!widget.properties.FeedUrl) { return (
@@ -56,7 +58,7 @@ function VideoStreamWidget({ widget }: VideoStreamWidgetProps) { return (