mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-22 08:56:44 +00:00
feat(ui): add customizable download table columns
Add persisted column reordering, positional alignment, animated drag feedback, and resize cleanup for the download table. Keep row actions at the trailing edge, preserve RTL/LTR behavior, and include localized column controls and sidebar alignment fixes.
This commit is contained in:
+140
-110
@@ -9,12 +9,19 @@ import {
|
||||
formatDownloadTotal,
|
||||
resolveDownloadSizeDisplay
|
||||
} from '../utils/downloadProgress';
|
||||
import {
|
||||
COLUMN_ALIGNMENT_JUSTIFY,
|
||||
type DownloadColumnAlignment,
|
||||
type DownloadTableColumnKey
|
||||
} from '../utils/downloadTableColumns';
|
||||
|
||||
interface DownloadItemProps {
|
||||
download: DownloadItemType;
|
||||
index: number;
|
||||
queueIndex: number;
|
||||
queueLength: number;
|
||||
columnOrder: DownloadTableColumnKey[];
|
||||
columnAlignments: Record<DownloadTableColumnKey, DownloadColumnAlignment>;
|
||||
tableGridTemplate: string;
|
||||
tableMinWidth: number;
|
||||
setContextMenu: (menu: { x: number; y: number; id: string }) => void;
|
||||
@@ -31,6 +38,8 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
index,
|
||||
queueIndex,
|
||||
queueLength,
|
||||
columnOrder,
|
||||
columnAlignments,
|
||||
tableGridTemplate,
|
||||
tableMinWidth,
|
||||
setContextMenu,
|
||||
@@ -83,35 +92,34 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
unit: sizeDisplay.unit ?? '',
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`download-row group cursor-default relative ${index % 2 !== 0 ? 'striped' : ''} ${isSelected ? 'is-selected' : ''}`}
|
||||
style={{ gridTemplateColumns: tableGridTemplate, minWidth: tableMinWidth }}
|
||||
onClick={(e) => onClick(e, download)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, id: download.id });
|
||||
}}
|
||||
>
|
||||
<div className="download-file-cell">
|
||||
<span className="shrink-0 text-text-muted">
|
||||
{getCategoryIcon(download.category)}
|
||||
</span>
|
||||
<span className="download-file-name" title={download.fileName}>
|
||||
{download.fileName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
const columnStyle = (key: DownloadTableColumnKey): React.CSSProperties => ({
|
||||
'--column-justify': COLUMN_ALIGNMENT_JUSTIFY[columnAlignments[key]],
|
||||
} as React.CSSProperties);
|
||||
|
||||
const cells: Record<DownloadTableColumnKey, React.ReactNode> = {
|
||||
'File Name': (
|
||||
<div
|
||||
className="download-cell-truncate download-size-cell tabular-nums"
|
||||
title={hasDownloadedAmount
|
||||
? downloadedSizeLabel
|
||||
: completedSizeLabel}
|
||||
aria-label={hasDownloadedAmount
|
||||
? downloadedSizeLabel
|
||||
: completedSizeLabel}
|
||||
className="download-column-cell download-file-cell download-column-file-name"
|
||||
style={columnStyle('File Name')}
|
||||
>
|
||||
<div className="download-size-content">
|
||||
<div className="download-cell-content">
|
||||
<span className="shrink-0 text-text-muted">
|
||||
{getCategoryIcon(download.category)}
|
||||
</span>
|
||||
<span className="download-file-name" title={download.fileName}>
|
||||
{download.fileName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
Size: (
|
||||
<div
|
||||
className="download-column-cell download-cell-truncate download-size-cell tabular-nums"
|
||||
style={columnStyle('Size')}
|
||||
title={hasDownloadedAmount ? downloadedSizeLabel : completedSizeLabel}
|
||||
aria-label={hasDownloadedAmount ? downloadedSizeLabel : completedSizeLabel}
|
||||
>
|
||||
<div className="download-cell-content download-size-content">
|
||||
{hasDownloadedAmount ? (
|
||||
<span className="download-size-progress">
|
||||
<span className={downloadProgressColorClass(download.status)}>{sizeDisplay.downloaded}</span>
|
||||
@@ -125,16 +133,22 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="download-status-cell">
|
||||
),
|
||||
Status: (
|
||||
<div
|
||||
className="download-column-cell download-status-cell"
|
||||
style={columnStyle('Status')}
|
||||
>
|
||||
{download.status === 'completed' ? (
|
||||
<span className="download-status download-status-completed" title={downloadStatusLabel}>{downloadStatusLabel}</span>
|
||||
<span className="download-cell-content download-status download-status-completed" title={downloadStatusLabel}>
|
||||
{downloadStatusLabel}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<div className="download-cell-content download-status-content">
|
||||
<div className="download-progress-track">
|
||||
<div
|
||||
className={`download-progress-fill ${
|
||||
download.status === 'paused' ? 'paused' :
|
||||
download.status === 'paused' ? 'paused' :
|
||||
download.status === 'processing' ? 'processing' :
|
||||
download.status === 'queued' || download.status === 'staged' ? 'queued' :
|
||||
download.status === 'retrying' ? 'retrying' : ''
|
||||
@@ -143,22 +157,22 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
title={
|
||||
download.lastError && (download.status === 'failed' || download.status === 'retrying')
|
||||
? download.lastError
|
||||
: (download.status === 'queued' || download.status === 'staged') && queueIndex !== -1
|
||||
? `${downloadStatusLabel} #${queueIndex + 1}`
|
||||
: download.status === 'downloading'
|
||||
? displayPercent
|
||||
: download.status === 'processing'
|
||||
? downloadStatusLabel
|
||||
: downloadStatusLabel
|
||||
}
|
||||
className={`download-status flex items-center gap-1.5 ${
|
||||
download.status === 'paused' ? 'download-status-paused' :
|
||||
download.status === 'failed' ? 'download-status-failed' :
|
||||
title={
|
||||
download.lastError && (download.status === 'failed' || download.status === 'retrying')
|
||||
? download.lastError
|
||||
: (download.status === 'queued' || download.status === 'staged') && queueIndex !== -1
|
||||
? `${downloadStatusLabel} #${queueIndex + 1}`
|
||||
: download.status === 'downloading'
|
||||
? displayPercent
|
||||
: download.status === 'processing'
|
||||
? downloadStatusLabel
|
||||
: downloadStatusLabel
|
||||
}
|
||||
className={`download-status flex items-center gap-1.5 ${
|
||||
download.status === 'paused' ? 'download-status-paused' :
|
||||
download.status === 'failed' ? 'download-status-failed' :
|
||||
download.status === 'processing' ? 'download-status-processing' :
|
||||
download.status === 'downloading' ? 'download-status-downloading' :
|
||||
download.status === 'downloading' ? 'download-status-downloading' :
|
||||
download.status === 'queued' || download.status === 'staged' ? 'download-status-queued' :
|
||||
download.status === 'retrying' ? 'download-status-retrying' : ''
|
||||
}`}
|
||||
@@ -178,83 +192,99 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
downloadStatusLabel
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="download-cell-truncate">
|
||||
<span
|
||||
className="tabular-nums"
|
||||
title={displaySpeed}
|
||||
>
|
||||
),
|
||||
Speed: (
|
||||
<div className="download-column-cell download-cell-truncate" style={columnStyle('Speed')}>
|
||||
<span className="download-cell-content tabular-nums" title={displaySpeed}>
|
||||
{displaySpeed}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="download-cell-truncate">
|
||||
<span
|
||||
className="tabular-nums"
|
||||
title={displayEta}
|
||||
>
|
||||
),
|
||||
ETA: (
|
||||
<div className="download-column-cell download-cell-truncate" style={columnStyle('ETA')}>
|
||||
<span className="download-cell-content tabular-nums" title={displayEta}>
|
||||
{displayEta}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="download-cell-right">
|
||||
),
|
||||
'Date Added': (
|
||||
<div className="download-column-cell download-cell-right download-column-date-added" style={columnStyle('Date Added')}>
|
||||
<span
|
||||
className="truncate group-hover:hidden tabular-nums"
|
||||
className="download-cell-content download-date-value group-hover:hidden tabular-nums"
|
||||
title={download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
|
||||
>
|
||||
{download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
|
||||
</span>
|
||||
|
||||
<div
|
||||
className="hidden group-hover:flex items-center justify-end gap-0.5 w-full ml-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{(download.status === 'queued' || download.status === 'staged') && queueIndex !== -1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onMoveInQueue(download.id, 'up')}
|
||||
disabled={queueIndex === 0}
|
||||
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
||||
title={t($ => $.downloads.actions.moveUp)}
|
||||
>
|
||||
<ArrowUp size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onMoveInQueue(download.id, 'down')}
|
||||
disabled={queueIndex === queueLength - 1}
|
||||
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
||||
title={t($ => $.downloads.actions.moveDown)}
|
||||
>
|
||||
<ArrowDown size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{canPauseDownload(download.status) && (
|
||||
<button onClick={() => handlePause(download.id)} className="app-icon-button h-7 w-7" title={t($ => $.downloads.actions.pause)}>
|
||||
<Pause size={14} fill="currentColor" />
|
||||
</button>
|
||||
)}
|
||||
{canStartDownload(download.status) && (
|
||||
<button onClick={() => handleResume(download)} className="app-icon-button h-7 w-7" title={download.status === 'paused' ? t($ => $.downloads.actions.resume) : t($ => $.downloads.actions.start)}>
|
||||
<Play size={14} fill="currentColor" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, id: download.id });
|
||||
}}
|
||||
className="app-icon-button h-7 w-7"
|
||||
title={t($ => $.downloads.actions.options)}
|
||||
>
|
||||
<MoreVertical size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
const rowActions = (
|
||||
<div
|
||||
className="download-row-actions hidden group-hover:flex items-center gap-0.5"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{(download.status === 'queued' || download.status === 'staged') && queueIndex !== -1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onMoveInQueue(download.id, 'up')}
|
||||
disabled={queueIndex === 0}
|
||||
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
||||
title={t($ => $.downloads.actions.moveUp)}
|
||||
>
|
||||
<ArrowUp size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onMoveInQueue(download.id, 'down')}
|
||||
disabled={queueIndex === queueLength - 1}
|
||||
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
||||
title={t($ => $.downloads.actions.moveDown)}
|
||||
>
|
||||
<ArrowDown size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{canPauseDownload(download.status) && (
|
||||
<button onClick={() => handlePause(download.id)} className="app-icon-button h-7 w-7" title={t($ => $.downloads.actions.pause)}>
|
||||
<Pause size={14} fill="currentColor" />
|
||||
</button>
|
||||
)}
|
||||
{canStartDownload(download.status) && (
|
||||
<button onClick={() => handleResume(download)} className="app-icon-button h-7 w-7" title={download.status === 'paused' ? t($ => $.downloads.actions.resume) : t($ => $.downloads.actions.start)}>
|
||||
<Play size={14} fill="currentColor" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, id: download.id });
|
||||
}}
|
||||
className="app-icon-button h-7 w-7"
|
||||
title={t($ => $.downloads.actions.options)}
|
||||
>
|
||||
<MoreVertical size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`download-row group cursor-default relative ${index % 2 !== 0 ? 'striped' : ''} ${isSelected ? 'is-selected' : ''}`}
|
||||
style={{ gridTemplateColumns: tableGridTemplate, minWidth: tableMinWidth }}
|
||||
onClick={(e) => onClick(e, download)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, id: download.id });
|
||||
}}
|
||||
>
|
||||
{columnOrder.map(columnKey => (
|
||||
<React.Fragment key={columnKey}>{cells[columnKey]}</React.Fragment>
|
||||
))}
|
||||
{rowActions}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user