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