mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user