mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-04 13:49:19 +01:00
feat: add filter and sorting functionality to torrents table
This commit is contained in:
@@ -91,6 +91,10 @@
|
||||
"html-entities": "^2.3.3",
|
||||
"i18next": "^22.5.1",
|
||||
"immer": "^10.0.2",
|
||||
"js-file-download": "^0.4.12",
|
||||
"mantine-react-table": "^1.3.4",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"next": "13.4.12",
|
||||
"next-auth": "^4.23.0",
|
||||
"next-i18next": "^14.0.0",
|
||||
|
||||
@@ -41,12 +41,22 @@
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"isCompleted": "Downloading",
|
||||
"name": "Name",
|
||||
"dateAdded": "Added On",
|
||||
"size": "Size",
|
||||
"download": "Down",
|
||||
"upload": "Up",
|
||||
"estimatedTimeOfArrival": "ETA",
|
||||
"progress": "Progress"
|
||||
"progress": "Progress",
|
||||
"totalUploaded": "Total Upload",
|
||||
"totalDownloaded": "Total Download",
|
||||
"ratio": "Ratio",
|
||||
"seeds": "Seeds (Connected)",
|
||||
"peers": "Peers (Connected)",
|
||||
"label": "Label",
|
||||
"state": "State",
|
||||
"stateMessage": "State Message"
|
||||
},
|
||||
"item": {
|
||||
"text": "Managed by {{appName}}, {{ratio}} ratio"
|
||||
|
||||
@@ -6,12 +6,11 @@ import {
|
||||
Group,
|
||||
List,
|
||||
MantineColor,
|
||||
Popover,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
createStyles,
|
||||
useMantineTheme,
|
||||
useMantineTheme
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconAffiliate,
|
||||
@@ -24,8 +23,6 @@ import {
|
||||
IconUpload,
|
||||
} from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
|
||||
import { calculateETA } from '~/tools/client/calculateEta';
|
||||
import { humanFileSize } from '~/tools/humanFileSize';
|
||||
import { AppType } from '~/types/app';
|
||||
|
||||
@@ -35,89 +32,7 @@ interface TorrentQueueItemProps {
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const BitTorrentQueueItem = ({ torrent, width, app }: TorrentQueueItemProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation('modules/torrents-status');
|
||||
|
||||
const size = torrent.totalSelected;
|
||||
return (
|
||||
<Popover
|
||||
withArrow
|
||||
withinPortal
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
transitionProps={{
|
||||
transition: 'pop',
|
||||
}}
|
||||
>
|
||||
<Popover.Target>
|
||||
<tr key={torrent.id} style={{ cursor: 'pointer' }}>
|
||||
<td>
|
||||
<Text
|
||||
style={{
|
||||
maxWidth: '30vw',
|
||||
}}
|
||||
size="xs"
|
||||
lineClamp={1}
|
||||
>
|
||||
{torrent.name}
|
||||
</Text>
|
||||
{app && (
|
||||
<Text size="xs" color="dimmed">
|
||||
{t('card.table.item.text', {
|
||||
appName: app.name,
|
||||
ratio: torrent.ratio.toFixed(2),
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Text className={classes.noTextBreak} size="xs">
|
||||
{humanFileSize(size, false)}
|
||||
</Text>
|
||||
</td>
|
||||
{width > MIN_WIDTH_MOBILE && (
|
||||
<td>
|
||||
<Text className={classes.noTextBreak} size="xs">
|
||||
{torrent.downloadSpeed > 0 ? `${humanFileSize(torrent.downloadSpeed,false)}/s` : '-'}
|
||||
</Text>
|
||||
</td>
|
||||
)}
|
||||
{width > MIN_WIDTH_MOBILE && (
|
||||
<td>
|
||||
<Text className={classes.noTextBreak} size="xs">
|
||||
{torrent.uploadSpeed > 0 ? `${humanFileSize(torrent.uploadSpeed,false)}/s` : '-'}
|
||||
</Text>
|
||||
</td>
|
||||
)}
|
||||
{width > MIN_WIDTH_MOBILE && (
|
||||
<td>
|
||||
<Text className={classes.noTextBreak} size="xs">
|
||||
{torrent.eta <= 0 ? '∞' : calculateETA(torrent.eta)}
|
||||
</Text>
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<Text className={classes.noTextBreak}>{(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>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<TorrentQueuePopover torrent={torrent} app={app} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const TorrentQueuePopover = ({ torrent, app }: Omit<TorrentQueueItemProps, 'width'>) => {
|
||||
export const TorrentQueuePopover = ({ torrent, app }: Omit<TorrentQueueItemProps, 'width'>) => {
|
||||
const { t } = useTranslation('modules/torrents-status');
|
||||
const { colors } = useMantineTheme();
|
||||
|
||||
|
||||
@@ -1,31 +1,42 @@
|
||||
import { NormalizedTorrent, TorrentState } from '@ctrl/shared-torrent';
|
||||
import {
|
||||
MRT_Table,
|
||||
useMantineReactTable,
|
||||
type MRT_ColumnDef,
|
||||
} from 'mantine-react-table';
|
||||
|
||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
||||
import {
|
||||
Badge,
|
||||
Center,
|
||||
createStyles,
|
||||
Flex,
|
||||
Group,
|
||||
Loader,
|
||||
Popover,
|
||||
Progress,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
import { IconFileDownload, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { IconFileDownload } from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { useCardStyles } from '~/components/layout/Common/useCardStyles';
|
||||
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
|
||||
import { calculateETA } from '~/tools/client/calculateEta';
|
||||
import { humanFileSize } from '~/tools/humanFileSize';
|
||||
import { NormalizedDownloadQueueResponse } from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||
import { AppIntegrationType } from '~/types/app';
|
||||
|
||||
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
|
||||
import { defineWidget } from '../helper';
|
||||
import { IWidget } from '../widgets';
|
||||
import { BitTorrentQueueItem } from './TorrentQueueItem';
|
||||
import { TorrentQueuePopover } from './TorrentQueueItem';
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
@@ -44,7 +55,8 @@ const definition = defineWidget({
|
||||
type: 'switch',
|
||||
defaultValue: true,
|
||||
},
|
||||
speedLimitOfActiveTorrents: { // Unit : kB/s
|
||||
speedLimitOfActiveTorrents: {
|
||||
// Unit : kB/s
|
||||
type: 'number',
|
||||
defaultValue: 10,
|
||||
},
|
||||
@@ -98,6 +110,137 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
dataUpdatedAt: number;
|
||||
} = useGetDownloadClientsQueue();
|
||||
|
||||
let torrents: NormalizedTorrent[] = [];
|
||||
if(!(isError || !data || data.apps.length === 0 || Object.values(data.apps).length < 1)) {
|
||||
torrents = data.apps.flatMap((app) => (app.type === 'torrent' ? app.torrents : []))
|
||||
}
|
||||
|
||||
const filteredTorrents = filterTorrents(widget, torrents);
|
||||
|
||||
|
||||
const difference = new Date().getTime() - dataUpdatedAt;
|
||||
const duration = dayjs.duration(difference, 'ms');
|
||||
const humanizedDuration = duration.humanize();
|
||||
|
||||
const ratioGlobal = getTorrentsRatio(widget, torrents, false);
|
||||
const ratioWithFilter = getTorrentsRatio(widget, torrents, true);
|
||||
|
||||
const columns = useMemo<MRT_ColumnDef<NormalizedTorrent>[]>(() => [
|
||||
{
|
||||
id: "dateAdded",
|
||||
accessorFn: (row) => new Date(row.dateAdded),
|
||||
header: "dateAdded",
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: t('card.table.header.name'),
|
||||
Cell: ({ cell, row }) => (
|
||||
<Popover
|
||||
withArrow
|
||||
withinPortal
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
transitionProps={{
|
||||
transition: 'pop',
|
||||
}}
|
||||
>
|
||||
<Popover.Target>
|
||||
<Text
|
||||
style={{
|
||||
maxWidth: '30vw',
|
||||
}}
|
||||
size="xs"
|
||||
lineClamp={1}
|
||||
>
|
||||
{String(cell.getValue())}
|
||||
</Text>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<TorrentQueuePopover torrent={row.original} app={undefined} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'totalSize',
|
||||
header: t('card.table.header.size'),
|
||||
Cell: ({ cell }) => formatSize(Number(cell.getValue())),
|
||||
sortDescFirst: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'uploadSpeed',
|
||||
header: t('card.table.header.upload'),
|
||||
Cell: ({ cell }) => formatSpeed(Number(cell.getValue())),
|
||||
sortDescFirst: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'downloadSpeed',
|
||||
header: t('card.table.header.download'),
|
||||
Cell: ({ cell }) => formatSpeed(Number(cell.getValue())),
|
||||
sortDescFirst: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'eta',
|
||||
header: t('card.table.header.estimatedTimeOfArrival'),
|
||||
Cell: ({ cell }) => formatETA(Number(cell.getValue())),
|
||||
sortDescFirst: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'progress',
|
||||
header: t('card.table.header.progress'),
|
||||
Cell: ({ cell, row }) => (
|
||||
<Flex>
|
||||
<Text className={useStyles().classes.noTextBreak}>{(Number(cell.getValue()) * 100).toFixed(1)}%</Text>
|
||||
<Progress
|
||||
radius="lg"
|
||||
color={
|
||||
Number(cell.getValue()) === 1 ? 'green' : row.original.state === 'paused' ? 'yellow' : 'blue'
|
||||
}
|
||||
value={Number(cell.getValue()) * 100}
|
||||
size="lg"
|
||||
/>,
|
||||
</Flex>),
|
||||
sortDescFirst: true,
|
||||
},
|
||||
], []);
|
||||
|
||||
const torrentsTable = useMantineReactTable({
|
||||
columns,
|
||||
data: filteredTorrents,
|
||||
enablePagination: false,
|
||||
enableBottomToolbar: false,
|
||||
enableMultiSort: true,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enableSorting: true,
|
||||
initialState: {
|
||||
showColumnFilters: false,
|
||||
showGlobalFilter: false,
|
||||
density: 'xs',
|
||||
sorting: [{ id: 'dateAdded', desc: true }],
|
||||
columnVisibility: {
|
||||
isCompleted: false,
|
||||
dateAdded: false,
|
||||
uploadSpeed: false,
|
||||
downloadSpeed: false,
|
||||
eta: false,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
showColumnFilters: false,
|
||||
showGlobalFilter: false,
|
||||
density: 'xs',
|
||||
columnVisibility: {
|
||||
isCompleted: false,
|
||||
dateAdded: false,
|
||||
uploadSpeed: width > MIN_WIDTH_MOBILE,
|
||||
downloadSpeed: width > MIN_WIDTH_MOBILE,
|
||||
eta: width > MIN_WIDTH_MOBILE,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Stack>
|
||||
@@ -146,51 +289,10 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const torrents = data.apps.flatMap((app) => (app.type === 'torrent' ? app.torrents : []));
|
||||
const filteredTorrents = filterTorrents(widget, torrents);
|
||||
|
||||
const difference = new Date().getTime() - dataUpdatedAt;
|
||||
const duration = dayjs.duration(difference, 'ms');
|
||||
const humanizedDuration = duration.humanize();
|
||||
|
||||
const ratioGlobal = getTorrentsRatio(widget, torrents, false);
|
||||
const ratioWithFilter = getTorrentsRatio(widget, torrents, true);
|
||||
|
||||
return (
|
||||
<Flex direction="column" sx={{ height: '100%' }} ref={ref}>
|
||||
<ScrollArea sx={{ height: '100%', width: '100%' }} mb="xs">
|
||||
<Table striped highlightOnHover p="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredTorrents.map((torrent, index) => (
|
||||
<BitTorrentQueueItem key={index} torrent={torrent} width={width} app={undefined} />
|
||||
))}
|
||||
|
||||
{filteredTorrents.length !== torrents.length && (
|
||||
<tr className={classes.card}>
|
||||
<td colSpan={width > MIN_WIDTH_MOBILE ? 6 : 3}>
|
||||
<Flex gap="xs" align="center" justify="center">
|
||||
<IconInfoCircle opacity={0.7} size={18} />
|
||||
<Text align="center" color="dimmed">
|
||||
{t('card.table.body.filterHidingItems', {
|
||||
count: torrents.length - filteredTorrents.length,
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
<ScrollArea>
|
||||
<MRT_Table table={torrentsTable} />
|
||||
</ScrollArea>
|
||||
<Group spacing="sm">
|
||||
{data.apps.some((x) => !x.success) && (
|
||||
@@ -198,9 +300,8 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
{t('card.footer.error')}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Text color="dimmed" size="xs">
|
||||
{t('card.footer.lastUpdated', { time: humanizedDuration })}
|
||||
{t('card.footer.lastUpdated', { time: humanizedDuration })}
|
||||
{` - ${t('card.footer.ratioGlobal')} : ${
|
||||
ratioGlobal === -1 ? '∞' : ratioGlobal.toFixed(2)
|
||||
}`}
|
||||
@@ -217,7 +318,12 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
|
||||
let result = torrents;
|
||||
if (!widget.properties.displayCompletedTorrents) {
|
||||
result = result.filter((torrent) => !torrent.isCompleted || (widget.properties.displayActiveTorrents && torrent.uploadSpeed > widget.properties.speedLimitOfActiveTorrents * 1024));
|
||||
result = result.filter(
|
||||
(torrent) =>
|
||||
!torrent.isCompleted ||
|
||||
(widget.properties.displayActiveTorrents &&
|
||||
torrent.uploadSpeed > widget.properties.speedLimitOfActiveTorrents * 1024)
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.properties.labelFilter.length > 0) {
|
||||
@@ -279,4 +385,22 @@ export const getTorrentsRatio = (
|
||||
: -1;
|
||||
};
|
||||
|
||||
const formatSize = (sizeInBytes: number) => {
|
||||
return humanFileSize(sizeInBytes, false);
|
||||
};
|
||||
|
||||
const formatSpeed = (speedInBytesPerSecond: number) => {
|
||||
return `${humanFileSize(speedInBytesPerSecond, false)}/s`;
|
||||
};
|
||||
|
||||
const formatETA = (seconds: number) => {
|
||||
return calculateETA(seconds);
|
||||
};
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
noTextBreak: {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
}));
|
||||
|
||||
export default definition;
|
||||
|
||||
Reference in New Issue
Block a user