Add detail popover for torrents list

This commit is contained in:
Manuel
2023-01-18 21:24:55 +01:00
parent e950987359
commit 1bf3b1312b
8 changed files with 328 additions and 85 deletions

View File

@@ -1,14 +1,37 @@
/* eslint-disable @next/next/no-img-element */
import { NormalizedTorrent } from '@ctrl/shared-torrent';
import { Tooltip, Text, Progress, useMantineTheme } from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import {
Badge,
Flex,
Group,
MantineColor,
Popover,
Progress,
Text,
useMantineTheme,
} from '@mantine/core';
import { useDisclosure, useElementSize } from '@mantine/hooks';
import {
IconAffiliate,
IconDatabase,
IconDownload,
IconInfoCircle,
IconPercentage,
IconSortDescending,
IconUpload,
} from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { calculateETA } from '../../tools/calculateEta';
import { humanFileSize } from '../../tools/humanFileSize';
import { AppType } from '../../types/app';
interface TorrentQueueItemProps {
torrent: NormalizedTorrent;
app?: AppType;
}
export const BitTorrrentQueueItem = ({ torrent }: TorrentQueueItemProps) => {
export const BitTorrrentQueueItem = ({ torrent, app }: TorrentQueueItemProps) => {
const [popoverOpened, { open: openPopover, close: closePopover }] = useDisclosure(false);
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
const { width } = useElementSize();
@@ -18,17 +41,29 @@ export const BitTorrrentQueueItem = ({ torrent }: TorrentQueueItemProps) => {
return (
<tr key={torrent.id}>
<td>
<Tooltip position="top" withinPortal label={torrent.name}>
<Text
style={{
maxWidth: '30vw',
}}
size="xs"
lineClamp={1}
>
{torrent.name}
</Text>
</Tooltip>
<Popover opened={popoverOpened} width={350} position="top" withinPortal>
<Popover.Dropdown>
<TorrentQueuePopover torrent={torrent} app={app} />
</Popover.Dropdown>
<Popover.Target>
<div onMouseEnter={openPopover} onMouseLeave={closePopover}>
<Text
style={{
maxWidth: '30vw',
}}
size="xs"
lineClamp={1}
>
{torrent.name}
</Text>
{app && (
<Text size="xs" color="dimmed">
Managed by {app.name}, {torrent.ratio.toFixed(2)} ratio
</Text>
)}
</div>
</Popover.Target>
</Popover>
</td>
<td>
<Text size="xs">{humanFileSize(size)}</Text>
@@ -60,3 +95,98 @@ export const BitTorrrentQueueItem = ({ torrent }: TorrentQueueItemProps) => {
</tr>
);
};
const TorrentQueuePopover = ({ torrent, app }: TorrentQueueItemProps) => {
const { t } = useTranslation('modules/torrents-status');
const { colors } = useMantineTheme();
const RatioMetric = () => {
const color = (): MantineColor => {
if (torrent.ratio < 1) {
return colors.red[7];
}
if (torrent.ratio < 1.15) {
return colors.orange[7];
}
return colors.green[7];
};
return (
<Group spacing="xs">
<IconAffiliate size={16} />
<Group spacing={3}>
<Text>Ratio -</Text>
<Text color={color()} weight="bold">
{torrent.ratio.toFixed(2)}
</Text>
</Group>
</Group>
);
};
return (
<>
{app && (
<Group spacing={3}>
<Text size="xs" color="dimmed">
{t('card.popover.introductionPrefix')}
</Text>
<img src={app.appearance.iconUrl} alt="download client logo" width={15} height={15} />
<Text size="xs" color="dimmed">
{app.name}
</Text>
</Group>
)}
<Text mb="md" weight="bold">
{torrent.name}
</Text>
<RatioMetric />
<Group spacing="xs">
<IconSortDescending size={16} />
<Text>{t('card.popover.metrics.queuePosition', { position: torrent.queuePosition })}</Text>
</Group>
<Group spacing="xs">
<IconPercentage size={16} />
<Text>
{t('card.popover.metrics.progress', { progress: (torrent.progress * 100).toFixed(2) })}
</Text>
</Group>
<Group spacing="xs">
<IconDatabase size={16} />
<Text>
{t('card.popover.metrics.totalSelectedSize', {
totalSize: humanFileSize(torrent.totalSelected),
})}
</Text>
</Group>
<Group>
<Group spacing="xs">
<IconDownload size={16} />
<Text>{humanFileSize(torrent.totalDownloaded)}</Text>
</Group>
<Group spacing="xs">
<IconUpload size={16} />
<Text>{humanFileSize(torrent.totalUploaded)}</Text>
</Group>
</Group>
<Group spacing="xs">
<IconInfoCircle size={16} />
<Text>{t('card.popover.metrics.state', { state: torrent.stateMessage })}</Text>
</Group>
<Flex mt="md" mb="xs" gap="sm">
{torrent.label && <Badge variant="outline">{torrent.label}</Badge>}
{torrent.isCompleted && (
<Badge variant="dot" color="green">
{t('card.popover.metrics.completed')}
</Badge>
)}
</Flex>
</>
);
};

View File

@@ -1,5 +1,6 @@
import { NormalizedTorrent } from '@ctrl/shared-torrent';
import {
Badge,
Center,
Flex,
Group,
@@ -20,6 +21,7 @@ import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import { useConfigContext } from '../../config/provider';
import { useGetTorrentData } from '../../hooks/widgets/torrents/useGetTorrentData';
import { NormalizedTorrentListResponse } from '../../types/api/NormalizedTorrentListResponse';
import { AppIntegrationType } from '../../types/app';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
@@ -76,7 +78,17 @@ function TorrentTile({ widget }: TorrentTileProps) {
[];
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
const { data, isError, isInitialLoading, dataUpdatedAt } = useGetTorrentData({
const {
data,
isError,
isInitialLoading,
dataUpdatedAt,
}: {
data: NormalizedTorrentListResponse | undefined;
isError: boolean;
isInitialLoading: boolean;
dataUpdatedAt: number;
} = useGetTorrentData({
appId: selectedAppId!,
refreshInterval: widget.properties.refreshInterval * 1000,
});
@@ -127,7 +139,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
);
}
if (!data || data.length < 1) {
if (!data || Object.values(data.torrents).length < 1) {
return (
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Title order={3}>{t('card.table.body.nothingFound')}</Title>
@@ -153,7 +165,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
return (
<Flex direction="column" sx={{ height: '100%' }}>
<ScrollArea sx={{ height: '100%', width: '100%' }}>
<ScrollArea sx={{ height: '100%', width: '100%' }} mb="xs">
<Table highlightOnHover p="sm">
<thead>
<tr>
@@ -166,15 +178,28 @@ function TorrentTile({ widget }: TorrentTileProps) {
</tr>
</thead>
<tbody>
{data.filter(filter).map((item: NormalizedTorrent, index: number) => (
<BitTorrrentQueueItem key={index} torrent={item} />
))}
{data.torrents.map((concatenatedTorrentList) => {
const app = config?.apps.find((x) => x.id === concatenatedTorrentList.appId);
return concatenatedTorrentList.torrents
.filter(filter)
.map((item: NormalizedTorrent, index: number) => (
<BitTorrrentQueueItem key={index} torrent={item} app={app} />
));
})}
</tbody>
</Table>
</ScrollArea>
<Text color="dimmed" size="xs">
Last updated {humanizedDuration} ago
</Text>
<Group spacing="sm">
{!data.allSuccess && (
<Badge variant="dot" color="red">
{t('card.footer.error')}
</Badge>
)}
<Text color="dimmed" size="xs">
{t('card.footer.lastUpdated', { time: humanizedDuration })}
</Text>
</Group>
</Flex>
);
}

View File

@@ -11,6 +11,7 @@ import { useEffect, useState } from 'react';
import { useConfigContext } from '../../config/provider';
import { useSetSafeInterval } from '../../hooks/useSetSafeInterval';
import { humanFileSize } from '../../tools/humanFileSize';
import { NormalizedTorrentListResponse } from '../../types/api/NormalizedTorrentListResponse';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
@@ -60,7 +61,8 @@ function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTrafficTileProps) {
axios
.post('/api/modules/torrents')
.then((response) => {
setTorrents(response.data);
const responseData: NormalizedTorrentListResponse = response.data;
setTorrents(responseData.torrents.flatMap((x) => x.torrents));
})
.catch((error) => {
if (error.status === 401) return;