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 type AddDownloadAction
} from '../store/useDownloadStore'; } from '../store/useDownloadStore';
import { useSettingsStore } from '../store/useSettingsStore'; 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 { open } from '@tauri-apps/plugin-dialog';
import { invokeCommand as invoke } from '../ipc'; import { invokeCommand as invoke } from '../ipc';
import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResolutionModal'; import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResolutionModal';
@@ -73,7 +73,6 @@ export const AddDownloadsModal = () => {
const [pendingAction, setPendingAction] = useState<AddDownloadAction>({ type: 'start-now' }); const [pendingAction, setPendingAction] = useState<AddDownloadAction>({ type: 'start-now' });
const [pendingUseSharedDestination, setPendingUseSharedDestination] = useState(false); const [pendingUseSharedDestination, setPendingUseSharedDestination] = useState(false);
const [resolvedLocation, setResolvedLocation] = useState(''); const [resolvedLocation, setResolvedLocation] = useState('');
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
const [isQueueMenuOpen, setIsQueueMenuOpen] = useState(false); const [isQueueMenuOpen, setIsQueueMenuOpen] = useState(false);
const actionMenuRef = useRef<HTMLDivElement>(null); const actionMenuRef = useRef<HTMLDivElement>(null);
@@ -119,7 +118,6 @@ export const AddDownloadsModal = () => {
].filter(Boolean).join('\n')); ].filter(Boolean).join('\n'));
setCookies(pendingAddCookies); setCookies(pendingAddCookies);
setMirrors(''); setMirrors('');
setIsActionMenuOpen(false);
setIsQueueMenuOpen(false); setIsQueueMenuOpen(false);
} else { } else {
setUrls(''); setUrls('');
@@ -135,31 +133,29 @@ export const AddDownloadsModal = () => {
]); ]);
useEffect(() => { useEffect(() => {
if (!isActionMenuOpen) return; if (!isQueueMenuOpen) return;
const closeMenu = (event: PointerEvent) => { const closeMenu = (event: PointerEvent) => {
if (!actionMenuRef.current?.contains(event.target as Node)) { if (!actionMenuRef.current?.contains(event.target as Node)) {
setIsActionMenuOpen(false);
setIsQueueMenuOpen(false); setIsQueueMenuOpen(false);
} }
}; };
window.addEventListener('pointerdown', closeMenu); window.addEventListener('pointerdown', closeMenu);
return () => window.removeEventListener('pointerdown', closeMenu); return () => window.removeEventListener('pointerdown', closeMenu);
}, [isActionMenuOpen]); }, [isQueueMenuOpen]);
useEffect(() => { useEffect(() => {
if (!isActionMenuOpen && !showingDuplicates) return; if (!isQueueMenuOpen && !showingDuplicates) return;
const closeOnEscape = (event: KeyboardEvent) => { const closeOnEscape = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return; if (event.key !== 'Escape') return;
if (showingDuplicates) { if (showingDuplicates) {
setShowingDuplicates(false); setShowingDuplicates(false);
} else { } else {
setIsActionMenuOpen(false);
setIsQueueMenuOpen(false); setIsQueueMenuOpen(false);
} }
}; };
window.addEventListener('keydown', closeOnEscape); window.addEventListener('keydown', closeOnEscape);
return () => window.removeEventListener('keydown', closeOnEscape); return () => window.removeEventListener('keydown', closeOnEscape);
}, [isActionMenuOpen, showingDuplicates]); }, [isQueueMenuOpen, showingDuplicates]);
useEffect(() => { useEffect(() => {
if (!saveLocation) return; if (!saveLocation) return;
@@ -945,68 +941,34 @@ export const AddDownloadsModal = () => {
<div className="relative"> <div className="relative">
<button <button
type="button" type="button"
onClick={() => { onClick={() => setIsQueueMenuOpen(open => !open)}
setIsActionMenuOpen(open => !open);
setIsQueueMenuOpen(false);
}}
disabled={parsedItems.length === 0} disabled={parsedItems.length === 0}
className="add-download-button add-download-button-secondary px-4 text-xs" 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-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> </button>
{isActionMenuOpen && ( {isQueueMenuOpen && (
<div <div
role="menu" role="menu"
className="app-modal absolute bottom-full right-0 z-[70] mb-2 min-w-[200px] overflow-visible py-1.5 text-xs" className="app-modal absolute bottom-full right-0 z-[70] mb-2 min-w-[200px] overflow-visible py-1.5 text-xs"
> >
<button {queues.map(queue => (
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">
<button <button
key={queue.id}
type="button" type="button"
role="menuitem" role="menuitem"
aria-haspopup="menu" onClick={() => {
aria-expanded={isQueueMenuOpen} setIsQueueMenuOpen(false);
onClick={() => setIsQueueMenuOpen(open => !open)} void handleAction({ type: 'add-to-queue', queueId: queue.id });
className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left hover:bg-item-hover" }}
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"> <span className="truncate">{queue.name}</span>
<ListPlus size={14} />
<span>Add to Queue</span>
</div>
<ChevronRight size={14} />
</button> </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>
)} )}
</div> </div>
+2 -2
View File
@@ -154,7 +154,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const filteredDownloads = downloads.filter((d: DownloadItem) => { const filteredDownloads = downloads.filter((d: DownloadItem) => {
if (filter.startsWith('queue:')) { if (filter.startsWith('queue:')) {
return d.queueId === filter.replace('queue:', ''); return d.queueId === filter.replace('queue:', '') && d.status !== 'completed';
} }
switch (filter) { switch (filter) {
case 'all': return true; case 'all': return true;
@@ -375,7 +375,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
{contextMenu && contextItem && ( {contextMenu && contextItem && (
<div <div
role="menu" 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={{ style={{
top: Math.min(contextMenu.y, window.innerHeight - 300), top: Math.min(contextMenu.y, window.innerHeight - 300),
left: Math.min(contextMenu.x, window.innerWidth - 200) 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 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 () => { it('adds to the selected queue without dispatching', async () => {
await useDownloadStore.getState().addDownload({ await useDownloadStore.getState().addDownload({
-3
View File
@@ -143,7 +143,6 @@ export type { DownloadItem, Queue };
export type ExtensionDownloadRequest = ExtensionDownload; export type ExtensionDownloadRequest = ExtensionDownload;
export type AddDownloadAction = export type AddDownloadAction =
| { type: 'start-now' } | { type: 'start-now' }
| { type: 'add-to-list' }
| { type: 'add-to-queue'; queueId: string }; | { type: 'add-to-queue'; queueId: string };
export type DownloadDraft = Omit<DownloadItem, 'status' | 'queueId' | 'hasBeenDispatched'>; 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 }); get().updateDownload(item.id, { hasBeenDispatched: true });
} }
info(`Download ${item.id} started`); info(`Download ${item.id} started`);
} else {
info(`Download ${item.id} added to list`);
} }
}, },
applyProperties: async (id, updates) => { applyProperties: async (id, updates) => {