mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-19 07:36:17 +00:00
fix(ui): fence stale allocation status
- keep paused and completed rows authoritative over transient allocation state - preserve allocation feedback for failed-download retries - expose the table allocation phase as an accessible indeterminate progressbar
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
} from '../utils/downloadActions';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import { isAllocationPhaseVisible } from '../utils/downloads';
|
||||
import { formatDateTime } from '../utils/dateTime';
|
||||
import {
|
||||
downloadProgressColorClass,
|
||||
@@ -83,6 +84,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
const [isActionHovered, setIsActionHovered] = React.useState(false);
|
||||
const [isActionFocused, setIsActionFocused] = React.useState(false);
|
||||
const [actionPosition, setActionPosition] = React.useState<React.CSSProperties | undefined>();
|
||||
const allocationVisible = isAllocationPhaseVisible(allocationPending, download.status);
|
||||
const hasRowActions = download.status !== 'completed';
|
||||
const isBulkSelection = isSelected && selectedDownloadCount > 1;
|
||||
const pauseSelectionCount = isBulkSelection && selectedActionCounts.pause > 0
|
||||
@@ -202,7 +204,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
status: download.status,
|
||||
});
|
||||
const displayPercent = `${(displayFraction * 100).toFixed(0)}%`;
|
||||
const displaySpeed = allocationPending
|
||||
const displaySpeed = allocationVisible
|
||||
? '-'
|
||||
: download.status === 'seeding'
|
||||
? liveProgress?.upload_speed ?? '-'
|
||||
@@ -211,7 +213,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
: download.status === 'processing'
|
||||
? t($ => $.downloads.values.processing)
|
||||
: '-';
|
||||
const displayEta = allocationPending
|
||||
const displayEta = allocationVisible
|
||||
? '-'
|
||||
: download.status === 'seeding'
|
||||
? typeof download.torrentSeedRemaining === 'number' && Number.isFinite(download.torrentSeedRemaining) && download.torrentSeedRemaining > 0
|
||||
@@ -234,7 +236,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
const value = download.status === 'completed' ? formatDownloadTotal(sizeDisplay) : sizeDisplay.fallback;
|
||||
return value === 'Unknown' ? t($ => $.addDownloads.unknown) : value;
|
||||
})();
|
||||
const downloadStatusLabel = allocationPending
|
||||
const downloadStatusLabel = allocationVisible
|
||||
? t($ => $.downloads.status.allocatingFiles)
|
||||
: t($ => $.downloads.status[download.status]);
|
||||
const visibleErrorStatusLabel = download.lastErrorKind === 'nameResolution'
|
||||
@@ -326,10 +328,16 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
</div>
|
||||
) : (
|
||||
<div className="download-cell-content download-status-content">
|
||||
<div className="download-progress-track" aria-label={allocationPending ? downloadStatusLabel : undefined}>
|
||||
<div
|
||||
className="download-progress-track"
|
||||
aria-label={allocationVisible ? downloadStatusLabel : undefined}
|
||||
aria-busy={allocationVisible ? true : undefined}
|
||||
aria-valuetext={allocationVisible ? downloadStatusLabel : undefined}
|
||||
role={allocationVisible ? 'progressbar' : undefined}
|
||||
>
|
||||
<div
|
||||
className={`download-progress-fill ${
|
||||
allocationPending ? 'allocating' :
|
||||
allocationVisible ? 'allocating' :
|
||||
download.status === 'paused' ? 'paused' :
|
||||
download.status === 'seeding' ? 'seeding' :
|
||||
download.status === 'processing' ? 'processing' :
|
||||
@@ -338,12 +346,12 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
download.status === 'queued' || download.status === 'staged' ? 'queued' :
|
||||
download.status === 'retrying' ? 'retrying' : ''
|
||||
}`}
|
||||
style={{ width: allocationPending ? undefined : `${displayFraction * 100}%` }}
|
||||
style={{ width: allocationVisible ? undefined : `${displayFraction * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
title={
|
||||
allocationPending
|
||||
allocationVisible
|
||||
? downloadStatusLabel
|
||||
: download.lastError && (
|
||||
download.status === 'failed'
|
||||
@@ -360,7 +368,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
: downloadStatusLabel
|
||||
}
|
||||
className={`download-status flex items-center gap-1.5 ${
|
||||
allocationPending ? 'download-status-downloading' :
|
||||
allocationVisible ? 'download-status-downloading' :
|
||||
download.status === 'paused' ? 'download-status-paused' :
|
||||
download.status === 'seeding' ? 'download-status-seeding' :
|
||||
download.status === 'failed' ? 'download-status-failed' :
|
||||
@@ -372,7 +380,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
download.status === 'retrying' ? 'download-status-retrying' : ''
|
||||
}`}
|
||||
>
|
||||
{allocationPending ? (
|
||||
{allocationVisible ? (
|
||||
<>
|
||||
<RefreshCw size={12} className="animate-spin motion-reduce:animate-none shrink-0" aria-hidden="true" />
|
||||
<span className="truncate">{downloadStatusLabel}</span>
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
TORRENT_ENCRYPTION_POLICY_DISABLED,
|
||||
TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION,
|
||||
TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO,
|
||||
isAllocationPhaseVisible,
|
||||
type TorrentEncryptionPolicy,
|
||||
type TorrentFileAllocation,
|
||||
} from '../utils/downloads';
|
||||
@@ -1140,7 +1141,7 @@ export const PropertiesWindowApp = () => {
|
||||
});
|
||||
const isPromptFooter = footerActions.includes('keepEditing');
|
||||
const fileSelectionEditingEnabled = editingEnabled && isTorrentFileSelectionEditable(snapshot.status);
|
||||
const allocationPending = snapshot.allocationPending === true;
|
||||
const allocationPending = isAllocationPhaseVisible(snapshot.allocationPending === true, snapshot.status);
|
||||
const total = snapshot.size || (snapshot.totalBytes === undefined
|
||||
? t($ => $.addDownloads.unknownSize)
|
||||
: `${snapshot.totalIsEstimate ? '~' : ''}${formatDownloadBytes(snapshot.totalBytes)}`);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
canonicalizeDownloadFileName,
|
||||
categoryForDownload,
|
||||
categoryForFileName,
|
||||
isAllocationPhaseVisible,
|
||||
isValidTorrentExcludeTrackerList,
|
||||
isValidTorrentTrackerList,
|
||||
normalizeTorrentEncryptionPolicy,
|
||||
@@ -107,6 +108,16 @@ describe('download persistence progress snapshots', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('allocation phase visibility', () => {
|
||||
it('does not override paused or completed statuses', () => {
|
||||
expect(isAllocationPhaseVisible(true, 'ready')).toBe(true);
|
||||
expect(isAllocationPhaseVisible(true, 'failed')).toBe(true);
|
||||
expect(isAllocationPhaseVisible(true, 'paused')).toBe(false);
|
||||
expect(isAllocationPhaseVisible(true, 'completed')).toBe(false);
|
||||
expect(isAllocationPhaseVisible(false, 'downloading')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Torrent tracker input validation', () => {
|
||||
it('accepts supported trackers separated by lines or commas', () => {
|
||||
expect(isValidTorrentTrackerList(
|
||||
|
||||
@@ -45,6 +45,16 @@ export const isActiveDownloadStatus = (status: DownloadStatus): boolean =>
|
||||
export const isTransferActiveStatus = (status: DownloadStatus): boolean =>
|
||||
status === 'downloading' || status === 'processing' || status === 'verifying' || status === 'seeding' || status === 'retrying';
|
||||
|
||||
/**
|
||||
* A transient allocation flag must never replace a terminal or user-paused
|
||||
* status in the UI. Failed rows remain eligible because retry admission can
|
||||
* begin from the failed state before the backend accepts the new lifecycle.
|
||||
*/
|
||||
export const isAllocationPhaseVisible = (
|
||||
allocationPending: boolean,
|
||||
status: DownloadStatus,
|
||||
): boolean => allocationPending && status !== 'completed' && status !== 'paused';
|
||||
|
||||
export const DOWNLOAD_CONNECTIONS_MIN = 1;
|
||||
export const DOWNLOAD_CONNECTIONS_MAX = 16;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user