mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-28 11:37: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';
|
} from '../utils/downloadActions';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSettingsStore } from '../store/useSettingsStore';
|
import { useSettingsStore } from '../store/useSettingsStore';
|
||||||
|
import { isAllocationPhaseVisible } from '../utils/downloads';
|
||||||
import { formatDateTime } from '../utils/dateTime';
|
import { formatDateTime } from '../utils/dateTime';
|
||||||
import {
|
import {
|
||||||
downloadProgressColorClass,
|
downloadProgressColorClass,
|
||||||
@@ -83,6 +84,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
const [isActionHovered, setIsActionHovered] = React.useState(false);
|
const [isActionHovered, setIsActionHovered] = React.useState(false);
|
||||||
const [isActionFocused, setIsActionFocused] = React.useState(false);
|
const [isActionFocused, setIsActionFocused] = React.useState(false);
|
||||||
const [actionPosition, setActionPosition] = React.useState<React.CSSProperties | undefined>();
|
const [actionPosition, setActionPosition] = React.useState<React.CSSProperties | undefined>();
|
||||||
|
const allocationVisible = isAllocationPhaseVisible(allocationPending, download.status);
|
||||||
const hasRowActions = download.status !== 'completed';
|
const hasRowActions = download.status !== 'completed';
|
||||||
const isBulkSelection = isSelected && selectedDownloadCount > 1;
|
const isBulkSelection = isSelected && selectedDownloadCount > 1;
|
||||||
const pauseSelectionCount = isBulkSelection && selectedActionCounts.pause > 0
|
const pauseSelectionCount = isBulkSelection && selectedActionCounts.pause > 0
|
||||||
@@ -202,7 +204,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
status: download.status,
|
status: download.status,
|
||||||
});
|
});
|
||||||
const displayPercent = `${(displayFraction * 100).toFixed(0)}%`;
|
const displayPercent = `${(displayFraction * 100).toFixed(0)}%`;
|
||||||
const displaySpeed = allocationPending
|
const displaySpeed = allocationVisible
|
||||||
? '-'
|
? '-'
|
||||||
: download.status === 'seeding'
|
: download.status === 'seeding'
|
||||||
? liveProgress?.upload_speed ?? '-'
|
? liveProgress?.upload_speed ?? '-'
|
||||||
@@ -211,7 +213,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
: download.status === 'processing'
|
: download.status === 'processing'
|
||||||
? t($ => $.downloads.values.processing)
|
? t($ => $.downloads.values.processing)
|
||||||
: '-';
|
: '-';
|
||||||
const displayEta = allocationPending
|
const displayEta = allocationVisible
|
||||||
? '-'
|
? '-'
|
||||||
: download.status === 'seeding'
|
: download.status === 'seeding'
|
||||||
? typeof download.torrentSeedRemaining === 'number' && Number.isFinite(download.torrentSeedRemaining) && download.torrentSeedRemaining > 0
|
? 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;
|
const value = download.status === 'completed' ? formatDownloadTotal(sizeDisplay) : sizeDisplay.fallback;
|
||||||
return value === 'Unknown' ? t($ => $.addDownloads.unknown) : value;
|
return value === 'Unknown' ? t($ => $.addDownloads.unknown) : value;
|
||||||
})();
|
})();
|
||||||
const downloadStatusLabel = allocationPending
|
const downloadStatusLabel = allocationVisible
|
||||||
? t($ => $.downloads.status.allocatingFiles)
|
? t($ => $.downloads.status.allocatingFiles)
|
||||||
: t($ => $.downloads.status[download.status]);
|
: t($ => $.downloads.status[download.status]);
|
||||||
const visibleErrorStatusLabel = download.lastErrorKind === 'nameResolution'
|
const visibleErrorStatusLabel = download.lastErrorKind === 'nameResolution'
|
||||||
@@ -326,10 +328,16 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="download-cell-content download-status-content">
|
<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
|
<div
|
||||||
className={`download-progress-fill ${
|
className={`download-progress-fill ${
|
||||||
allocationPending ? 'allocating' :
|
allocationVisible ? 'allocating' :
|
||||||
download.status === 'paused' ? 'paused' :
|
download.status === 'paused' ? 'paused' :
|
||||||
download.status === 'seeding' ? 'seeding' :
|
download.status === 'seeding' ? 'seeding' :
|
||||||
download.status === 'processing' ? 'processing' :
|
download.status === 'processing' ? 'processing' :
|
||||||
@@ -338,12 +346,12 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
download.status === 'queued' || download.status === 'staged' ? 'queued' :
|
download.status === 'queued' || download.status === 'staged' ? 'queued' :
|
||||||
download.status === 'retrying' ? 'retrying' : ''
|
download.status === 'retrying' ? 'retrying' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{ width: allocationPending ? undefined : `${displayFraction * 100}%` }}
|
style={{ width: allocationVisible ? undefined : `${displayFraction * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
title={
|
title={
|
||||||
allocationPending
|
allocationVisible
|
||||||
? downloadStatusLabel
|
? downloadStatusLabel
|
||||||
: download.lastError && (
|
: download.lastError && (
|
||||||
download.status === 'failed'
|
download.status === 'failed'
|
||||||
@@ -360,7 +368,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
: downloadStatusLabel
|
: downloadStatusLabel
|
||||||
}
|
}
|
||||||
className={`download-status flex items-center gap-1.5 ${
|
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 === 'paused' ? 'download-status-paused' :
|
||||||
download.status === 'seeding' ? 'download-status-seeding' :
|
download.status === 'seeding' ? 'download-status-seeding' :
|
||||||
download.status === 'failed' ? 'download-status-failed' :
|
download.status === 'failed' ? 'download-status-failed' :
|
||||||
@@ -372,7 +380,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
download.status === 'retrying' ? 'download-status-retrying' : ''
|
download.status === 'retrying' ? 'download-status-retrying' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{allocationPending ? (
|
{allocationVisible ? (
|
||||||
<>
|
<>
|
||||||
<RefreshCw size={12} className="animate-spin motion-reduce:animate-none shrink-0" aria-hidden="true" />
|
<RefreshCw size={12} className="animate-spin motion-reduce:animate-none shrink-0" aria-hidden="true" />
|
||||||
<span className="truncate">{downloadStatusLabel}</span>
|
<span className="truncate">{downloadStatusLabel}</span>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
TORRENT_ENCRYPTION_POLICY_DISABLED,
|
TORRENT_ENCRYPTION_POLICY_DISABLED,
|
||||||
TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION,
|
TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION,
|
||||||
TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO,
|
TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO,
|
||||||
|
isAllocationPhaseVisible,
|
||||||
type TorrentEncryptionPolicy,
|
type TorrentEncryptionPolicy,
|
||||||
type TorrentFileAllocation,
|
type TorrentFileAllocation,
|
||||||
} from '../utils/downloads';
|
} from '../utils/downloads';
|
||||||
@@ -1140,7 +1141,7 @@ export const PropertiesWindowApp = () => {
|
|||||||
});
|
});
|
||||||
const isPromptFooter = footerActions.includes('keepEditing');
|
const isPromptFooter = footerActions.includes('keepEditing');
|
||||||
const fileSelectionEditingEnabled = editingEnabled && isTorrentFileSelectionEditable(snapshot.status);
|
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
|
const total = snapshot.size || (snapshot.totalBytes === undefined
|
||||||
? t($ => $.addDownloads.unknownSize)
|
? t($ => $.addDownloads.unknownSize)
|
||||||
: `${snapshot.totalIsEstimate ? '~' : ''}${formatDownloadBytes(snapshot.totalBytes)}`);
|
: `${snapshot.totalIsEstimate ? '~' : ''}${formatDownloadBytes(snapshot.totalBytes)}`);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
canonicalizeDownloadFileName,
|
canonicalizeDownloadFileName,
|
||||||
categoryForDownload,
|
categoryForDownload,
|
||||||
categoryForFileName,
|
categoryForFileName,
|
||||||
|
isAllocationPhaseVisible,
|
||||||
isValidTorrentExcludeTrackerList,
|
isValidTorrentExcludeTrackerList,
|
||||||
isValidTorrentTrackerList,
|
isValidTorrentTrackerList,
|
||||||
normalizeTorrentEncryptionPolicy,
|
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', () => {
|
describe('Torrent tracker input validation', () => {
|
||||||
it('accepts supported trackers separated by lines or commas', () => {
|
it('accepts supported trackers separated by lines or commas', () => {
|
||||||
expect(isValidTorrentTrackerList(
|
expect(isValidTorrentTrackerList(
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ export const isActiveDownloadStatus = (status: DownloadStatus): boolean =>
|
|||||||
export const isTransferActiveStatus = (status: DownloadStatus): boolean =>
|
export const isTransferActiveStatus = (status: DownloadStatus): boolean =>
|
||||||
status === 'downloading' || status === 'processing' || status === 'verifying' || status === 'seeding' || status === 'retrying';
|
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_MIN = 1;
|
||||||
export const DOWNLOAD_CONNECTIONS_MAX = 16;
|
export const DOWNLOAD_CONNECTIONS_MAX = 16;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user