feat(ui): merge 'Add to list' into 'Add to queue' and fix display bugs

- Remove 'Add to list' function from UI and state store
- Replace 'Add to...' button with 'Add to queue' menu in AddDownloadsModal
- Prevent completed items from displaying in queue views
- Fix right-click context menu overflow clipping the queue submenu
- Gracefully discard completed items when batch-assigning to queues
This commit is contained in:
NimBold
2026-06-21 13:35:18 +03:30
parent 9e8c4aacf7
commit bf28029d2e
4 changed files with 21 additions and 76 deletions
+19 -57
View File
@@ -5,7 +5,7 @@ import {
type AddDownloadAction
} from '../store/useDownloadStore';
import { useSettingsStore } from '../store/useSettingsStore';
import { FolderPlus, Settings, Shield, RefreshCw, FileText, HardDrive, Database, Link, ArrowRight, Play, ChevronDown, ChevronRight, Video, Film, Music, ListPlus, Rows3, type LucideIcon } from 'lucide-react';
import { FolderPlus, Settings, Shield, RefreshCw, FileText, HardDrive, Database, Link, ArrowRight, Play, ChevronDown, ChevronRight, Video, Film, Music, type LucideIcon } from 'lucide-react';
import { open } from '@tauri-apps/plugin-dialog';
import { invokeCommand as invoke } from '../ipc';
import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResolutionModal';
@@ -73,7 +73,6 @@ export const AddDownloadsModal = () => {
const [pendingAction, setPendingAction] = useState<AddDownloadAction>({ type: 'start-now' });
const [pendingUseSharedDestination, setPendingUseSharedDestination] = useState(false);
const [resolvedLocation, setResolvedLocation] = useState('');
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
const [isQueueMenuOpen, setIsQueueMenuOpen] = useState(false);
const actionMenuRef = useRef<HTMLDivElement>(null);
@@ -119,7 +118,6 @@ export const AddDownloadsModal = () => {
].filter(Boolean).join('\n'));
setCookies(pendingAddCookies);
setMirrors('');
setIsActionMenuOpen(false);
setIsQueueMenuOpen(false);
} else {
setUrls('');
@@ -135,31 +133,29 @@ export const AddDownloadsModal = () => {
]);
useEffect(() => {
if (!isActionMenuOpen) return;
if (!isQueueMenuOpen) return;
const closeMenu = (event: PointerEvent) => {
if (!actionMenuRef.current?.contains(event.target as Node)) {
setIsActionMenuOpen(false);
setIsQueueMenuOpen(false);
}
};
window.addEventListener('pointerdown', closeMenu);
return () => window.removeEventListener('pointerdown', closeMenu);
}, [isActionMenuOpen]);
}, [isQueueMenuOpen]);
useEffect(() => {
if (!isActionMenuOpen && !showingDuplicates) return;
if (!isQueueMenuOpen && !showingDuplicates) return;
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
if (showingDuplicates) {
setShowingDuplicates(false);
} else {
setIsActionMenuOpen(false);
setIsQueueMenuOpen(false);
}
};
window.addEventListener('keydown', closeOnEscape);
return () => window.removeEventListener('keydown', closeOnEscape);
}, [isActionMenuOpen, showingDuplicates]);
}, [isQueueMenuOpen, showingDuplicates]);
useEffect(() => {
if (!saveLocation) return;
@@ -945,68 +941,34 @@ export const AddDownloadsModal = () => {
<div className="relative">
<button
type="button"
onClick={() => {
setIsActionMenuOpen(open => !open);
setIsQueueMenuOpen(false);
}}
onClick={() => setIsQueueMenuOpen(open => !open)}
disabled={parsedItems.length === 0}
className="add-download-button add-download-button-secondary px-4 text-xs"
aria-label="Add to..."
aria-label="Add to queue"
aria-haspopup="menu"
aria-expanded={isActionMenuOpen}
aria-expanded={isQueueMenuOpen}
>
Add to... <ChevronDown size={14} className="ml-1" />
Add to queue <ChevronDown size={14} className="ml-1" />
</button>
{isActionMenuOpen && (
{isQueueMenuOpen && (
<div
role="menu"
className="app-modal absolute bottom-full right-0 z-[70] mb-2 min-w-[200px] overflow-visible py-1.5 text-xs"
>
<button
type="button"
role="menuitem"
onClick={() => {
setIsActionMenuOpen(false);
void handleAction({ type: 'add-to-list' });
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-item-hover"
>
<Rows3 size={14} />
<span>Add to List</span>
</button>
<div className="group relative">
{queues.map(queue => (
<button
key={queue.id}
type="button"
role="menuitem"
aria-haspopup="menu"
aria-expanded={isQueueMenuOpen}
onClick={() => setIsQueueMenuOpen(open => !open)}
className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left hover:bg-item-hover"
onClick={() => {
setIsQueueMenuOpen(false);
void handleAction({ type: 'add-to-queue', queueId: queue.id });
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-item-hover"
>
<div className="flex items-center gap-2">
<ListPlus size={14} />
<span>Add to Queue</span>
</div>
<ChevronRight size={14} />
<span className="truncate">{queue.name}</span>
</button>
<div className={`app-modal absolute bottom-0 right-[calc(100%+4px)] z-[80] min-w-[160px] py-1.5 ${isQueueMenuOpen ? 'block' : 'hidden group-hover:block'}`}>
{queues.map(queue => (
<button
key={queue.id}
type="button"
role="menuitem"
onClick={() => {
setIsActionMenuOpen(false);
setIsQueueMenuOpen(false);
void handleAction({ type: 'add-to-queue', queueId: queue.id });
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-item-hover"
>
<span className="truncate">{queue.name}</span>
</button>
))}
</div>
</div>
))}
</div>
)}
</div>
+2 -2
View File
@@ -154,7 +154,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const filteredDownloads = downloads.filter((d: DownloadItem) => {
if (filter.startsWith('queue:')) {
return d.queueId === filter.replace('queue:', '');
return d.queueId === filter.replace('queue:', '') && d.status !== 'completed';
}
switch (filter) {
case 'all': return true;
@@ -375,7 +375,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
{contextMenu && contextItem && (
<div
role="menu"
className="app-modal fixed z-50 min-w-[180px] overflow-hidden py-1.5 text-[12px] font-medium text-text-primary"
className="app-modal fixed z-50 min-w-[180px] overflow-visible py-1.5 text-[12px] font-medium text-text-primary"
style={{
top: Math.min(contextMenu.y, window.innerHeight - 300),
left: Math.min(contextMenu.x, window.innerWidth - 200)
-14
View File
@@ -102,20 +102,6 @@ describe('useDownloadStore', () => {
expect(useDownloadStore.getState().backendRegisteredIds.has('1')).toBe(true); // Re-registered by dispatchItem
});
it('adds to the list in the main queue without dispatching', async () => {
await useDownloadStore.getState().addDownload({
id: 'list-1',
url: 'https://example.com/list.bin',
fileName: 'list.bin',
category: 'Other',
dateAdded: ''
}, { type: 'add-to-list' });
const item = useDownloadStore.getState().downloads[0];
expect(item.status).toBe('ready');
expect(item.queueId).toBe('00000000-0000-0000-0000-000000000001');
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything());
});
it('adds to the selected queue without dispatching', async () => {
await useDownloadStore.getState().addDownload({
-3
View File
@@ -143,7 +143,6 @@ export type { DownloadItem, Queue };
export type ExtensionDownloadRequest = ExtensionDownload;
export type AddDownloadAction =
| { type: 'start-now' }
| { type: 'add-to-list' }
| { type: 'add-to-queue'; queueId: string };
export type DownloadDraft = Omit<DownloadItem, 'status' | 'queueId' | 'hasBeenDispatched'>;
@@ -305,8 +304,6 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
get().updateDownload(item.id, { hasBeenDispatched: true });
}
info(`Download ${item.id} started`);
} else {
info(`Download ${item.id} added to list`);
}
},
applyProperties: async (id, updates) => {