feat: rework downloads widget with mantine-datatable
- Replace mantine-react-table with mantine-datatable for a cleaner, lighter table - Add right-click context menu with pause/resume/delete/copy actions - Add row expansion for inline detail view (replaces modal) - Implement responsive scaling based on widget width - Add footer filter panel with client and status filtering - Show status badges with color-coded indicators - Display speed indicators with icons in column cells - Add global ratio, total speeds, and item count in footer - Sort always enabled (removed confusing enableRowSorting toggle) - Reorder default columns to prioritize useful information - Update docs and translations
This commit is contained in:
7 files changed
+856
-911
No files matched your search
@@ -4,6 +4,7 @@ description: 'Allows you to view and manage your Downloads from both Torrent and
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import { aria2Integration } from '@site/docs/integrations/aria2';
|
||||
import { delugeIntegration } from '@site/docs/integrations/deluge';
|
||||
import { nzbgetIntegration } from '@site/docs/integrations/nzbget';
|
||||
import { qBittorentIntegration } from '@site/docs/integrations/q-bittorent';
|
||||
@@ -14,9 +15,6 @@ import { AddingWidget } from '@site/src/components/widgets/adding';
|
||||
import { WidgetConfig } from '@site/src/components/widgets/configuration';
|
||||
import { WidgetHeader } from '@site/src/components/widgets/header';
|
||||
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
|
||||
import { IconAd } from "@tabler/icons-react";
|
||||
import TabItem from '@theme/TabItem';
|
||||
import Tabs from '@theme/Tabs';
|
||||
import { downloadsWidget } from '.';
|
||||
|
||||
|
||||
@@ -25,15 +23,18 @@ import { downloadsWidget } from '.';
|
||||
categories={['Torrent client', 'Usenet client', 'Downloads']}
|
||||
/>
|
||||
|
||||
This widget displays a list of torrents or usenet-articles that are currently being downloaded, along with their progress and other relevant information.
|
||||
This widget displays a list of torrents or usenet-articles that are currently being downloaded, along with their progress and other relevant information. It supports 7 download client integrations and provides a rich, responsive table interface.
|
||||
|
||||
### Screenshots
|
||||
### Features
|
||||
|
||||
import downloadsCard from './img/screenshot.png';
|
||||
|
||||
<div>
|
||||
<img src={downloadsCard} alt="downloads" width={128 * 4} />
|
||||
</div>
|
||||
- **Responsive table** — columns automatically adapt to widget width, hiding less important columns at smaller sizes
|
||||
- **Sortable columns** — click any column header to sort ascending/descending
|
||||
- **Right-click context menu** — pause, resume, delete downloads, or copy download names directly from the context menu
|
||||
- **Row expansion** — click any row to see full details inline without opening a separate modal
|
||||
- **Client filtering** — filter downloads by integration or status using the footer filter panel
|
||||
- **Global controls** — pause/resume all downloads across all clients from the footer bar
|
||||
- **Status indicators** — color-coded badges show download state at a glance
|
||||
- **Speed overview** — total download and upload speeds displayed in the footer
|
||||
|
||||
### Supported Integrations
|
||||
|
||||
@@ -52,16 +53,21 @@ import downloadsCard from './img/screenshot.png';
|
||||
}, {
|
||||
integration: qBittorentIntegration,
|
||||
note: "Torrent client"
|
||||
}, {
|
||||
integration: aria2Integration,
|
||||
note: "Multi-protocol download client"
|
||||
}, {
|
||||
integration: slskdIntegration,
|
||||
note: "Soulseek client"
|
||||
note: "Soulseek client (read-only)"
|
||||
}]} />
|
||||
|
||||
### Interactions
|
||||
|
||||
- Pause / Resume one download by clicking on the according icon.
|
||||
- Delete one download by clicking on the trash icon. (Can also delete the files)
|
||||
- Pause / Resume all downloads by using the bottom button.
|
||||
- **Right-click** any download to open a context menu with pause/resume, delete, and copy actions
|
||||
- **Click** any row to expand it and see full download details inline
|
||||
- **Pause / Resume** all downloads using the footer controls
|
||||
- **Filter** by integration or status using the filter button in the footer
|
||||
- **Sort** by clicking column headers (with persistent sort direction)
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -2,26 +2,23 @@ import { WidgetDefinition } from "@site/src/types";
|
||||
import { IconDownload } from "@tabler/icons-react";
|
||||
|
||||
const columnsList = [
|
||||
"id",
|
||||
"actions",
|
||||
"added",
|
||||
"category",
|
||||
"downSpeed",
|
||||
"index",
|
||||
"integration",
|
||||
"name",
|
||||
"progress",
|
||||
"size",
|
||||
"downSpeed",
|
||||
"upSpeed",
|
||||
"time",
|
||||
"state",
|
||||
"added",
|
||||
"ratio",
|
||||
"received",
|
||||
"sent",
|
||||
"size",
|
||||
"state",
|
||||
"time",
|
||||
"category",
|
||||
"integration",
|
||||
"index",
|
||||
"type",
|
||||
"upSpeed",
|
||||
];
|
||||
const sortingExclusion = ["actions", "id", "state"];
|
||||
const columnsSort = columnsList.filter((column) => !sortingExclusion.some((exclusion) => exclusion === column));
|
||||
const sortColumns = columnsList.filter((c) => !["state", "category"].includes(c));
|
||||
|
||||
export const downloadsWidget: WidgetDefinition = {
|
||||
icon: IconDownload,
|
||||
@@ -32,25 +29,19 @@ export const downloadsWidget: WidgetDefinition = {
|
||||
items: [
|
||||
{
|
||||
name: "Columns to show",
|
||||
description: "Select the columns you want to display in the widget.",
|
||||
description: "Select the columns you want to display in the widget. Columns automatically hide/show based on widget width.",
|
||||
values: `List of columns: ${columnsList.join(", ")}`,
|
||||
defaultValue: ["integration", "name", "progress", "time", "actions"].join(", "),
|
||||
defaultValue: ["name", "progress", "downSpeed", "time", "state"].join(", "),
|
||||
},
|
||||
{
|
||||
name: "Enable items sorting",
|
||||
description: "Enables the sorting in the table.",
|
||||
values: { type: "boolean" },
|
||||
defaultValue: "no",
|
||||
},
|
||||
{
|
||||
name: "Column used for sorting by default",
|
||||
description: "By default it will sort by this column.",
|
||||
defaultValue: "type",
|
||||
values: `List of columns: ${columnsSort.join(", ")}`,
|
||||
name: "Default sort column",
|
||||
description: "The column used for initial sorting when the widget loads.",
|
||||
defaultValue: "progress",
|
||||
values: `List of columns: ${sortColumns.join(", ")}`,
|
||||
},
|
||||
{
|
||||
name: "Invert sorting",
|
||||
description: "This will invert the sorting order of the table.",
|
||||
description: "This will invert the default sorting order.",
|
||||
values: { type: "boolean" },
|
||||
defaultValue: "no",
|
||||
},
|
||||
@@ -76,13 +67,13 @@ export const downloadsWidget: WidgetDefinition = {
|
||||
name: "Hide completed torrent under this threshold (in kiB/s)",
|
||||
description:
|
||||
"This will hide completed torrent entries that have a download speed below the specified threshold.",
|
||||
values: "Any number above 1, 0 to disable",
|
||||
values: "Any number above 0, 0 to disable",
|
||||
defaultValue: "0",
|
||||
},
|
||||
{
|
||||
name: "Categories/names to filter",
|
||||
description: "You can filter the items by categories or names. Use a comma to separate multiple values.",
|
||||
values: "Comma-separated list of categories or names",
|
||||
name: "Categories/labels to filter",
|
||||
description: "You can filter the items by categories or labels. Use a comma to separate multiple values.",
|
||||
values: "Comma-separated list of categories or labels",
|
||||
defaultValue: "-",
|
||||
},
|
||||
{
|
||||
@@ -94,13 +85,13 @@ export const downloadsWidget: WidgetDefinition = {
|
||||
},
|
||||
{
|
||||
name: "Use filter to calculate Ratio",
|
||||
description: "This will use the filter to calculate the ratio of items shown in the widget.",
|
||||
description: "This will use the category filter when calculating the global torrent ratio.",
|
||||
values: { type: "boolean" },
|
||||
defaultValue: "yes",
|
||||
},
|
||||
{
|
||||
name: "Limit items per integration",
|
||||
description: "This will limit the number of items shown per integration, not globally",
|
||||
description: "This will limit the number of items shown per integration, not globally.",
|
||||
values: "Any number above 1",
|
||||
defaultValue: "50",
|
||||
},
|
||||
|
||||
@@ -74,11 +74,10 @@ const optionMapping: OptionMapping = {
|
||||
categoryFilter: (oldOptions) => ("labelFilter" in oldOptions ? oldOptions.labelFilter : undefined),
|
||||
filterIsWhitelist: (oldOptions) =>
|
||||
"labelFilterIsWhitelist" in oldOptions ? oldOptions.labelFilterIsWhitelist : undefined,
|
||||
enableRowSorting: (oldOptions) => ("rowSorting" in oldOptions ? oldOptions.rowSorting : undefined),
|
||||
showCompletedTorrent: (oldOptions) =>
|
||||
"displayCompletedTorrents" in oldOptions ? oldOptions.displayCompletedTorrents : undefined,
|
||||
columns: () => ["integration", "name", "progress", "time", "actions"],
|
||||
defaultSort: () => "type",
|
||||
columns: () => ["integration", "name", "progress", "time", "state"],
|
||||
defaultSort: () => "progress",
|
||||
descendingDefaultSort: () => false,
|
||||
showCompletedUsenet: () => true,
|
||||
showCompletedHttp: () => true,
|
||||
|
||||
@@ -3174,7 +3174,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"globalRatio": "Global Ratio"
|
||||
"globalRatio": "Global Ratio",
|
||||
"contextMenu": {
|
||||
"copyName": "Copy name",
|
||||
"copyId": "Copy ID",
|
||||
"clearFilters": "Clear filters"
|
||||
},
|
||||
"footer": {
|
||||
"filter": "Filter downloads"
|
||||
}
|
||||
},
|
||||
"mediaReleases": {
|
||||
"name": "Media releases",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,28 +8,37 @@ import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
const columnsList = [
|
||||
"id",
|
||||
"actions",
|
||||
"added",
|
||||
"category",
|
||||
"downSpeed",
|
||||
"index",
|
||||
"integration",
|
||||
"name",
|
||||
"progress",
|
||||
"size",
|
||||
"downSpeed",
|
||||
"upSpeed",
|
||||
"time",
|
||||
"state",
|
||||
"added",
|
||||
"ratio",
|
||||
"received",
|
||||
"sent",
|
||||
"size",
|
||||
"state",
|
||||
"time",
|
||||
"category",
|
||||
"integration",
|
||||
"index",
|
||||
"type",
|
||||
"upSpeed",
|
||||
] as const satisfies (keyof ExtendedDownloadClientItem)[];
|
||||
const sortingExclusion = ["actions", "id", "state"] as const satisfies readonly (typeof columnsList)[number][];
|
||||
const columnsSort = columnsList.filter((column) =>
|
||||
sortingExclusion.every((exclusion) => exclusion !== column),
|
||||
) as Exclude<typeof columnsList, (typeof sortingExclusion)[number]>;
|
||||
|
||||
const sortColumns = [
|
||||
"name",
|
||||
"progress",
|
||||
"size",
|
||||
"downSpeed",
|
||||
"upSpeed",
|
||||
"time",
|
||||
"added",
|
||||
"ratio",
|
||||
"received",
|
||||
"sent",
|
||||
"index",
|
||||
"type",
|
||||
] as const satisfies readonly (typeof columnsList)[number][];
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("downloads", {
|
||||
icon: IconDownload,
|
||||
@@ -38,19 +47,16 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
columns: factory.multiSelect({
|
||||
defaultValue: ["integration", "name", "progress", "time", "actions"],
|
||||
defaultValue: ["name", "progress", "downSpeed", "time", "state"],
|
||||
options: columnsList.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
||||
})),
|
||||
searchable: true,
|
||||
}),
|
||||
enableRowSorting: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
defaultSort: factory.select({
|
||||
defaultValue: "type",
|
||||
options: columnsSort.map((value) => ({
|
||||
defaultValue: "progress",
|
||||
options: sortColumns.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
||||
})),
|
||||
@@ -68,7 +74,6 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
||||
defaultValue: true,
|
||||
}),
|
||||
activeTorrentThreshold: factory.number({
|
||||
//in KiB/s
|
||||
validate: z.number().min(0),
|
||||
defaultValue: 0,
|
||||
step: 1,
|
||||
@@ -90,12 +95,6 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
||||
}),
|
||||
}),
|
||||
{
|
||||
defaultSort: {
|
||||
shouldHide: (options) => !options.enableRowSorting,
|
||||
},
|
||||
descendingDefaultSort: {
|
||||
shouldHide: (options) => !options.enableRowSorting,
|
||||
},
|
||||
showCompletedUsenet: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("usenet").some((kinds) => integrationKinds.includes(kinds)),
|
||||
|
||||
@@ -1,12 +1,52 @@
|
||||
/* Make the MantineReactTable transparent so the board card's opacity-controlled
|
||||
background shows through. MRT defaults to var(--mantine-color-body) which is opaque. */
|
||||
.downloads-widget-table,
|
||||
.downloads-widget-table .mantine-Table-table,
|
||||
.downloads-widget-table .mantine-Table-thead,
|
||||
.downloads-widget-table .mantine-Table-tbody,
|
||||
.downloads-widget-table .mantine-Table-tfoot,
|
||||
.downloads-widget-table .mantine-Table-tr,
|
||||
.downloads-widget-table th,
|
||||
.downloads-widget-table td {
|
||||
/* Make the DataTable transparent so the board card's opacity-controlled
|
||||
background shows through. The mantine-datatable defaults to
|
||||
var(--mantine-color-body) which is opaque. */
|
||||
.downloads-table,
|
||||
.downloads-table .mantine-datatable-table,
|
||||
.downloads-table .mantine-datatable-table thead,
|
||||
.downloads-table .mantine-datatable-table tbody,
|
||||
.downloads-table .mantine-datatable-table tfoot,
|
||||
.downloads-table th,
|
||||
.downloads-table td {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.downloads-table .mantine-datatable-table tr {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.downloads-table .mantine-datatable-table tbody tr:hover {
|
||||
background-color: color-mix(in srgb, var(--mantine-color-default-hover) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.downloads-table th {
|
||||
white-space: nowrap;
|
||||
background-color: color-mix(in srgb, var(--mantine-color-body) 60%, transparent) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.downloads-table td {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.downloads-table td .mantine-Text-root,
|
||||
.downloads-table td .mantine-Group-root {
|
||||
white-space: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.downloads-table .mantine-datatable-header-cell-sortable-icon {
|
||||
opacity: 0;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
|
||||
.downloads-table th:hover .mantine-datatable-header-cell-sortable-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Row expansion styling */
|
||||
.downloads-table .mantine-datatable-row-expansion-cell {
|
||||
background-color: color-mix(in srgb, var(--mantine-color-body) 40%, transparent) !important;
|
||||
}
|
||||
Reference in new issue
Block a user