fix(downloads): harden queue controls and resumable replacement

Prevent queue action clicks from triggering row double-clicks, preserve aria2/yt-dlp resumable sidecars during duplicate replacement, and make aria2 require resume instead of silently restarting.

Add a persisted, accessible Folders collapse control with reduced-motion animation and regression coverage for replacement sidecar handling.

Fixes #11

Fixes #12

Closes #13

Refs #14
This commit is contained in:
NimBold
2026-07-11 20:07:33 +03:30
parent 1922db8ea0
commit 33375df2ff
9 changed files with 182 additions and 19 deletions
+7 -5
View File
@@ -519,8 +519,7 @@ export const AddDownloadsModal = () => {
itemLocation,
finalFile,
platform.os
) &&
download.status !== 'failed'
)
) {
fileExistsInStore = true;
break;
@@ -662,8 +661,7 @@ export const AddDownloadsModal = () => {
itemLocation,
finalFile,
platform.os
) &&
download.status !== 'failed'
)
) {
existingItem = download;
break;
@@ -677,7 +675,11 @@ export const AddDownloadsModal = () => {
if (!existingItem) {
throw new Error(`Cannot replace ${finalFile}: file is not owned by a Firelink download.`);
}
await store.removeDownload(existingItem.id, true);
// Let the backend decide whether resumable sidecars still
// exist after stopping the old transfer. This avoids a race
// where a paused item finishes while the replacement is
// being prepared.
await store.removeDownload(existingItem.id, true, existingItem.status !== 'completed');
}
}
}
+1
View File
@@ -177,6 +177,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
<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 && (
+48 -8
View File
@@ -3,6 +3,7 @@ import {
Inbox, Zap, CheckCircle2, CircleDashed,
Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion,
List, CalendarClock, Gauge, Bug, Settings, Plus, Play, Pause, Edit2, Trash2, PanelLeft,
ChevronDown,
type LucideIcon
} from 'lucide-react';
import { useDownloadStore, DownloadCategory, Queue } from '../store/useDownloadStore';
@@ -28,6 +29,11 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
const [renamingQueueId, setRenamingQueueId] = useState<string | null>(null);
const [editingQueueName, setEditingQueueName] = useState('');
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const [foldersCollapsed, setFoldersCollapsed] = useState(() =>
window.localStorage.getItem('firelink-folders-collapsed') === 'true'
);
const foldersToggleRef = useRef<HTMLButtonElement>(null);
const foldersListRef = useRef<HTMLDivElement>(null);
const addInputRef = useRef<HTMLInputElement>(null);
const renameInputRef = useRef<HTMLInputElement>(null);
@@ -53,6 +59,16 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
if (renamingQueueId) renameInputRef.current?.focus();
}, [renamingQueueId]);
useEffect(() => {
window.localStorage.setItem('firelink-folders-collapsed', String(foldersCollapsed));
}, [foldersCollapsed]);
useEffect(() => {
if (foldersCollapsed && foldersListRef.current?.contains(document.activeElement)) {
foldersToggleRef.current?.focus();
}
}, [foldersCollapsed]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (document.querySelector('.app-modal-backdrop') || document.querySelector('.app-modal')) return;
@@ -214,14 +230,38 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
</section>
<section className="sidebar-section">
<div className="sidebar-section-label">Folders</div>
<NavItem icon={Music} label="Musics" filter="Musics" />
<NavItem icon={Film} label="Movies" filter="Movies" />
<NavItem icon={Archive} label="Compressed" filter="Compressed" />
<NavItem icon={FileText} label="Documents" filter="Documents" />
<NavItem icon={ImageIcon} label="Pictures" filter="Pictures" />
<NavItem icon={Box} label="Applications" filter="Applications" />
<NavItem icon={FileQuestion} label="Other" filter="Other" />
<button
type="button"
ref={foldersToggleRef}
className="sidebar-section-label sidebar-section-label-toggle"
aria-expanded={!foldersCollapsed}
aria-controls="sidebar-folders-list"
onClick={() => setFoldersCollapsed(collapsed => !collapsed)}
>
<span>Folders</span>
<ChevronDown
aria-hidden="true"
size={13}
className={`sidebar-section-chevron ${foldersCollapsed ? 'is-collapsed' : ''}`}
/>
</button>
<div
ref={foldersListRef}
id="sidebar-folders-list"
className={`sidebar-collapse-grid ${foldersCollapsed ? 'is-collapsed' : ''}`}
aria-hidden={foldersCollapsed}
inert={foldersCollapsed}
>
<div className="sidebar-collapse-content">
<NavItem icon={Music} label="Musics" filter="Musics" />
<NavItem icon={Film} label="Movies" filter="Movies" />
<NavItem icon={Archive} label="Compressed" filter="Compressed" />
<NavItem icon={FileText} label="Documents" filter="Documents" />
<NavItem icon={ImageIcon} label="Pictures" filter="Pictures" />
<NavItem icon={Box} label="Applications" filter="Applications" />
<NavItem icon={FileQuestion} label="Other" filter="Other" />
</div>
</div>
</section>
<section className="sidebar-section">