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
|
hide_title: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
import { aria2Integration } from '@site/docs/integrations/aria2';
|
||||||
import { delugeIntegration } from '@site/docs/integrations/deluge';
|
import { delugeIntegration } from '@site/docs/integrations/deluge';
|
||||||
import { nzbgetIntegration } from '@site/docs/integrations/nzbget';
|
import { nzbgetIntegration } from '@site/docs/integrations/nzbget';
|
||||||
import { qBittorentIntegration } from '@site/docs/integrations/q-bittorent';
|
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 { WidgetConfig } from '@site/src/components/widgets/configuration';
|
||||||
import { WidgetHeader } from '@site/src/components/widgets/header';
|
import { WidgetHeader } from '@site/src/components/widgets/header';
|
||||||
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
|
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 '.';
|
import { downloadsWidget } from '.';
|
||||||
|
|
||||||
|
|
||||||
@@ -25,15 +23,18 @@ import { downloadsWidget } from '.';
|
|||||||
categories={['Torrent client', 'Usenet client', 'Downloads']}
|
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';
|
- **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
|
||||||
<div>
|
- **Right-click context menu** — pause, resume, delete downloads, or copy download names directly from the context menu
|
||||||
<img src={downloadsCard} alt="downloads" width={128 * 4} />
|
- **Row expansion** — click any row to see full details inline without opening a separate modal
|
||||||
</div>
|
- **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
|
### Supported Integrations
|
||||||
|
|
||||||
@@ -52,16 +53,21 @@ import downloadsCard from './img/screenshot.png';
|
|||||||
}, {
|
}, {
|
||||||
integration: qBittorentIntegration,
|
integration: qBittorentIntegration,
|
||||||
note: "Torrent client"
|
note: "Torrent client"
|
||||||
|
}, {
|
||||||
|
integration: aria2Integration,
|
||||||
|
note: "Multi-protocol download client"
|
||||||
}, {
|
}, {
|
||||||
integration: slskdIntegration,
|
integration: slskdIntegration,
|
||||||
note: "Soulseek client"
|
note: "Soulseek client (read-only)"
|
||||||
}]} />
|
}]} />
|
||||||
|
|
||||||
### Interactions
|
### Interactions
|
||||||
|
|
||||||
- Pause / Resume one download by clicking on the according icon.
|
- **Right-click** any download to open a context menu with pause/resume, delete, and copy actions
|
||||||
- Delete one download by clicking on the trash icon. (Can also delete the files)
|
- **Click** any row to expand it and see full download details inline
|
||||||
- Pause / Resume all downloads by using the bottom button.
|
- **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
|
### Adding the widget
|
||||||
|
|
||||||
|
|||||||
@@ -2,26 +2,23 @@ import { WidgetDefinition } from "@site/src/types";
|
|||||||
import { IconDownload } from "@tabler/icons-react";
|
import { IconDownload } from "@tabler/icons-react";
|
||||||
|
|
||||||
const columnsList = [
|
const columnsList = [
|
||||||
"id",
|
|
||||||
"actions",
|
|
||||||
"added",
|
|
||||||
"category",
|
|
||||||
"downSpeed",
|
|
||||||
"index",
|
|
||||||
"integration",
|
|
||||||
"name",
|
"name",
|
||||||
"progress",
|
"progress",
|
||||||
|
"size",
|
||||||
|
"downSpeed",
|
||||||
|
"upSpeed",
|
||||||
|
"time",
|
||||||
|
"state",
|
||||||
|
"added",
|
||||||
"ratio",
|
"ratio",
|
||||||
"received",
|
"received",
|
||||||
"sent",
|
"sent",
|
||||||
"size",
|
"category",
|
||||||
"state",
|
"integration",
|
||||||
"time",
|
"index",
|
||||||
"type",
|
"type",
|
||||||
"upSpeed",
|
|
||||||
];
|
];
|
||||||
const sortingExclusion = ["actions", "id", "state"];
|
const sortColumns = columnsList.filter((c) => !["state", "category"].includes(c));
|
||||||
const columnsSort = columnsList.filter((column) => !sortingExclusion.some((exclusion) => exclusion === column));
|
|
||||||
|
|
||||||
export const downloadsWidget: WidgetDefinition = {
|
export const downloadsWidget: WidgetDefinition = {
|
||||||
icon: IconDownload,
|
icon: IconDownload,
|
||||||
@@ -32,25 +29,19 @@ export const downloadsWidget: WidgetDefinition = {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: "Columns to show",
|
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(", ")}`,
|
values: `List of columns: ${columnsList.join(", ")}`,
|
||||||
defaultValue: ["integration", "name", "progress", "time", "actions"].join(", "),
|
defaultValue: ["name", "progress", "downSpeed", "time", "state"].join(", "),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Enable items sorting",
|
name: "Default sort column",
|
||||||
description: "Enables the sorting in the table.",
|
description: "The column used for initial sorting when the widget loads.",
|
||||||
values: { type: "boolean" },
|
defaultValue: "progress",
|
||||||
defaultValue: "no",
|
values: `List of columns: ${sortColumns.join(", ")}`,
|
||||||
},
|
|
||||||
{
|
|
||||||
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: "Invert sorting",
|
name: "Invert sorting",
|
||||||
description: "This will invert the sorting order of the table.",
|
description: "This will invert the default sorting order.",
|
||||||
values: { type: "boolean" },
|
values: { type: "boolean" },
|
||||||
defaultValue: "no",
|
defaultValue: "no",
|
||||||
},
|
},
|
||||||
@@ -76,13 +67,13 @@ export const downloadsWidget: WidgetDefinition = {
|
|||||||
name: "Hide completed torrent under this threshold (in kiB/s)",
|
name: "Hide completed torrent under this threshold (in kiB/s)",
|
||||||
description:
|
description:
|
||||||
"This will hide completed torrent entries that have a download speed below the specified threshold.",
|
"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",
|
defaultValue: "0",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Categories/names to filter",
|
name: "Categories/labels to filter",
|
||||||
description: "You can filter the items by categories or names. Use a comma to separate multiple values.",
|
description: "You can filter the items by categories or labels. Use a comma to separate multiple values.",
|
||||||
values: "Comma-separated list of categories or names",
|
values: "Comma-separated list of categories or labels",
|
||||||
defaultValue: "-",
|
defaultValue: "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -94,13 +85,13 @@ export const downloadsWidget: WidgetDefinition = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Use filter to calculate Ratio",
|
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" },
|
values: { type: "boolean" },
|
||||||
defaultValue: "yes",
|
defaultValue: "yes",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Limit items per integration",
|
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",
|
values: "Any number above 1",
|
||||||
defaultValue: "50",
|
defaultValue: "50",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,11 +74,10 @@ const optionMapping: OptionMapping = {
|
|||||||
categoryFilter: (oldOptions) => ("labelFilter" in oldOptions ? oldOptions.labelFilter : undefined),
|
categoryFilter: (oldOptions) => ("labelFilter" in oldOptions ? oldOptions.labelFilter : undefined),
|
||||||
filterIsWhitelist: (oldOptions) =>
|
filterIsWhitelist: (oldOptions) =>
|
||||||
"labelFilterIsWhitelist" in oldOptions ? oldOptions.labelFilterIsWhitelist : undefined,
|
"labelFilterIsWhitelist" in oldOptions ? oldOptions.labelFilterIsWhitelist : undefined,
|
||||||
enableRowSorting: (oldOptions) => ("rowSorting" in oldOptions ? oldOptions.rowSorting : undefined),
|
|
||||||
showCompletedTorrent: (oldOptions) =>
|
showCompletedTorrent: (oldOptions) =>
|
||||||
"displayCompletedTorrents" in oldOptions ? oldOptions.displayCompletedTorrents : undefined,
|
"displayCompletedTorrents" in oldOptions ? oldOptions.displayCompletedTorrents : undefined,
|
||||||
columns: () => ["integration", "name", "progress", "time", "actions"],
|
columns: () => ["integration", "name", "progress", "time", "state"],
|
||||||
defaultSort: () => "type",
|
defaultSort: () => "progress",
|
||||||
descendingDefaultSort: () => false,
|
descendingDefaultSort: () => false,
|
||||||
showCompletedUsenet: () => true,
|
showCompletedUsenet: () => true,
|
||||||
showCompletedHttp: () => 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": {
|
"mediaReleases": {
|
||||||
"name": "Media releases",
|
"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";
|
import { optionsBuilder } from "../options";
|
||||||
|
|
||||||
const columnsList = [
|
const columnsList = [
|
||||||
"id",
|
|
||||||
"actions",
|
|
||||||
"added",
|
|
||||||
"category",
|
|
||||||
"downSpeed",
|
|
||||||
"index",
|
|
||||||
"integration",
|
|
||||||
"name",
|
"name",
|
||||||
"progress",
|
"progress",
|
||||||
|
"size",
|
||||||
|
"downSpeed",
|
||||||
|
"upSpeed",
|
||||||
|
"time",
|
||||||
|
"state",
|
||||||
|
"added",
|
||||||
"ratio",
|
"ratio",
|
||||||
"received",
|
"received",
|
||||||
"sent",
|
"sent",
|
||||||
"size",
|
"category",
|
||||||
"state",
|
"integration",
|
||||||
"time",
|
"index",
|
||||||
"type",
|
"type",
|
||||||
"upSpeed",
|
|
||||||
] as const satisfies (keyof ExtendedDownloadClientItem)[];
|
] as const satisfies (keyof ExtendedDownloadClientItem)[];
|
||||||
const sortingExclusion = ["actions", "id", "state"] as const satisfies readonly (typeof columnsList)[number][];
|
|
||||||
const columnsSort = columnsList.filter((column) =>
|
const sortColumns = [
|
||||||
sortingExclusion.every((exclusion) => exclusion !== column),
|
"name",
|
||||||
) as Exclude<typeof columnsList, (typeof sortingExclusion)[number]>;
|
"progress",
|
||||||
|
"size",
|
||||||
|
"downSpeed",
|
||||||
|
"upSpeed",
|
||||||
|
"time",
|
||||||
|
"added",
|
||||||
|
"ratio",
|
||||||
|
"received",
|
||||||
|
"sent",
|
||||||
|
"index",
|
||||||
|
"type",
|
||||||
|
] as const satisfies readonly (typeof columnsList)[number][];
|
||||||
|
|
||||||
export const { definition, componentLoader } = createWidgetDefinition("downloads", {
|
export const { definition, componentLoader } = createWidgetDefinition("downloads", {
|
||||||
icon: IconDownload,
|
icon: IconDownload,
|
||||||
@@ -38,19 +47,16 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
|||||||
return optionsBuilder.from(
|
return optionsBuilder.from(
|
||||||
(factory) => ({
|
(factory) => ({
|
||||||
columns: factory.multiSelect({
|
columns: factory.multiSelect({
|
||||||
defaultValue: ["integration", "name", "progress", "time", "actions"],
|
defaultValue: ["name", "progress", "downSpeed", "time", "state"],
|
||||||
options: columnsList.map((value) => ({
|
options: columnsList.map((value) => ({
|
||||||
value,
|
value,
|
||||||
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
||||||
})),
|
})),
|
||||||
searchable: true,
|
searchable: true,
|
||||||
}),
|
}),
|
||||||
enableRowSorting: factory.switch({
|
|
||||||
defaultValue: false,
|
|
||||||
}),
|
|
||||||
defaultSort: factory.select({
|
defaultSort: factory.select({
|
||||||
defaultValue: "type",
|
defaultValue: "progress",
|
||||||
options: columnsSort.map((value) => ({
|
options: sortColumns.map((value) => ({
|
||||||
value,
|
value,
|
||||||
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
||||||
})),
|
})),
|
||||||
@@ -68,7 +74,6 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
|||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
}),
|
}),
|
||||||
activeTorrentThreshold: factory.number({
|
activeTorrentThreshold: factory.number({
|
||||||
//in KiB/s
|
|
||||||
validate: z.number().min(0),
|
validate: z.number().min(0),
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
step: 1,
|
step: 1,
|
||||||
@@ -90,12 +95,6 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
defaultSort: {
|
|
||||||
shouldHide: (options) => !options.enableRowSorting,
|
|
||||||
},
|
|
||||||
descendingDefaultSort: {
|
|
||||||
shouldHide: (options) => !options.enableRowSorting,
|
|
||||||
},
|
|
||||||
showCompletedUsenet: {
|
showCompletedUsenet: {
|
||||||
shouldHide: (_, integrationKinds) =>
|
shouldHide: (_, integrationKinds) =>
|
||||||
!getIntegrationKindsByCategory("usenet").some((kinds) => integrationKinds.includes(kinds)),
|
!getIntegrationKindsByCategory("usenet").some((kinds) => integrationKinds.includes(kinds)),
|
||||||
|
|||||||
@@ -1,12 +1,52 @@
|
|||||||
/* Make the MantineReactTable transparent so the board card's opacity-controlled
|
/* Make the DataTable transparent so the board card's opacity-controlled
|
||||||
background shows through. MRT defaults to var(--mantine-color-body) which is opaque. */
|
background shows through. The mantine-datatable defaults to
|
||||||
.downloads-widget-table,
|
var(--mantine-color-body) which is opaque. */
|
||||||
.downloads-widget-table .mantine-Table-table,
|
.downloads-table,
|
||||||
.downloads-widget-table .mantine-Table-thead,
|
.downloads-table .mantine-datatable-table,
|
||||||
.downloads-widget-table .mantine-Table-tbody,
|
.downloads-table .mantine-datatable-table thead,
|
||||||
.downloads-widget-table .mantine-Table-tfoot,
|
.downloads-table .mantine-datatable-table tbody,
|
||||||
.downloads-widget-table .mantine-Table-tr,
|
.downloads-table .mantine-datatable-table tfoot,
|
||||||
.downloads-widget-table th,
|
.downloads-table th,
|
||||||
.downloads-widget-table td {
|
.downloads-table td {
|
||||||
background-color: transparent !important;
|
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