mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-26 02:27:14 +00:00
feat(ui): implement multi-select and fix finder reveal
- Fix 'Show in finder' for unfinished entries to open destination folder - Add 'Add to queue' submenu for unfinished items - Support Shift/Cmd modifiers for multi-selection in download list - Implement mass-action context menu for multi-selected downloads - Enhance DeleteConfirmationModal to handle mass deletions gracefully
This commit is contained in:
@@ -14,10 +14,10 @@ export const DeleteConfirmationModal: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFromList = async () => {
|
const handleRemoveFromList = async () => {
|
||||||
if (deleteModalState.downloadId) {
|
if (deleteModalState.downloadIds && deleteModalState.downloadIds.length > 0) {
|
||||||
setIsRemoving(true);
|
setIsRemoving(true);
|
||||||
try {
|
try {
|
||||||
await removeDownload(deleteModalState.downloadId, false);
|
await Promise.all(deleteModalState.downloadIds.map(id => removeDownload(id, false)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(`Remove failed: ${String(error)}`);
|
setErrorMessage(`Remove failed: ${String(error)}`);
|
||||||
setIsRemoving(false);
|
setIsRemoving(false);
|
||||||
@@ -28,10 +28,10 @@ export const DeleteConfirmationModal: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteFile = async () => {
|
const handleDeleteFile = async () => {
|
||||||
if (deleteModalState.downloadId) {
|
if (deleteModalState.downloadIds && deleteModalState.downloadIds.length > 0) {
|
||||||
setIsRemoving(true);
|
setIsRemoving(true);
|
||||||
try {
|
try {
|
||||||
await removeDownload(deleteModalState.downloadId, true);
|
await Promise.all(deleteModalState.downloadIds.map(id => removeDownload(id, true)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(`Delete failed: ${String(error)}`);
|
setErrorMessage(`Delete failed: ${String(error)}`);
|
||||||
setIsRemoving(false);
|
setIsRemoving(false);
|
||||||
@@ -55,7 +55,7 @@ export const DeleteConfirmationModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-5 py-6 flex-1 text-sm text-text-secondary leading-relaxed">
|
<div className="px-5 py-6 flex-1 text-sm text-text-secondary leading-relaxed">
|
||||||
Are you sure you want to remove this item from the list? You can also choose to delete the underlying file from your hard drive.
|
{`Are you sure you want to remove ${deleteModalState.downloadIds?.length && deleteModalState.downloadIds.length > 1 ? 'these ' + deleteModalState.downloadIds.length + ' items' : 'this item'} from the list? You can also choose to delete the underlying file from your hard drive.`}
|
||||||
{errorMessage && <div className="mt-3 text-xs text-red-400">{errorMessage}</div>}
|
{errorMessage && <div className="mt-3 text-xs text-red-400">{errorMessage}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface DownloadItemProps {
|
|||||||
handleResume: (item: DownloadItemType) => void;
|
handleResume: (item: DownloadItemType) => void;
|
||||||
handleDoubleClick: (item: DownloadItemType) => void;
|
handleDoubleClick: (item: DownloadItemType) => void;
|
||||||
getCategoryIcon: (category: string) => React.ReactNode;
|
getCategoryIcon: (category: string) => React.ReactNode;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: (e: React.MouseEvent, item: DownloadItemType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadItem = React.memo<DownloadItemProps>(({
|
export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||||
@@ -24,6 +26,8 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
handleResume,
|
handleResume,
|
||||||
handleDoubleClick,
|
handleDoubleClick,
|
||||||
getCategoryIcon,
|
getCategoryIcon,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const download = useDownloadStore(state => state.downloads.find(d => d.id === downloadId));
|
const download = useDownloadStore(state => state.downloads.find(d => d.id === downloadId));
|
||||||
const pendingOrder = useDownloadStore(state => state.pendingOrder);
|
const pendingOrder = useDownloadStore(state => state.pendingOrder);
|
||||||
@@ -66,8 +70,9 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`download-row group cursor-default relative ${index % 2 !== 0 ? 'striped' : ''}`}
|
className={`download-row group cursor-default relative ${index % 2 !== 0 ? 'striped' : ''} ${isSelected ? 'is-selected' : ''}`}
|
||||||
style={{ gridTemplateColumns: tableGridTemplate }}
|
style={{ gridTemplateColumns: tableGridTemplate }}
|
||||||
|
onClick={(e) => onClick(e, download)}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu({ x: e.clientX, y: e.clientY, id: download.id });
|
setContextMenu({ x: e.clientX, y: e.clientY, id: download.id });
|
||||||
|
|||||||
+272
-132
@@ -3,7 +3,7 @@ import { useDownloadStore, DownloadItem } from '../store/useDownloadStore';
|
|||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import { useSettingsStore } from '../store/useSettingsStore';
|
import { useSettingsStore } from '../store/useSettingsStore';
|
||||||
import { SidebarFilter } from './Sidebar';
|
import { SidebarFilter } from './Sidebar';
|
||||||
import { Play, Pause, Plus, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, PanelLeft, ArrowDownCircle, Command } from 'lucide-react';
|
import { Play, Pause, Plus, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, PanelLeft, ArrowDownCircle, Command, ChevronRight } from 'lucide-react';
|
||||||
import { DownloadItem as DownloadItemComponent } from './DownloadItem';
|
import { DownloadItem as DownloadItemComponent } from './DownloadItem';
|
||||||
import { invokeCommand as invoke } from '../ipc';
|
import { invokeCommand as invoke } from '../ipc';
|
||||||
import {
|
import {
|
||||||
@@ -16,13 +16,15 @@ interface DownloadTableProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||||
const { downloads, toggleAddModal, openDeleteModal, redownload } = useDownloadStore();
|
const { downloads, queues, updateDownload, toggleAddModal, openDeleteModal, redownload } = useDownloadStore();
|
||||||
const { isSidebarVisible, toggleSidebar } = useSettingsStore();
|
const { isSidebarVisible, toggleSidebar } = useSettingsStore();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
|
|
||||||
const isMac = navigator.userAgent.includes('Mac');
|
const isMac = navigator.userAgent.includes('Mac');
|
||||||
|
|
||||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
|
const [lastSelectedId, setLastSelectedId] = useState<string | null>(null);
|
||||||
const [columnWidths, setColumnWidths] = useState([340, 100, 220, 100, 80, 170]);
|
const [columnWidths, setColumnWidths] = useState([340, 100, 220, 100, 80, 170]);
|
||||||
const columnMinimums = [0, 58, 92, 58, 48, 112];
|
const columnMinimums = [0, 58, 92, 58, 48, 112];
|
||||||
const tableGridTemplate = columnWidths.map((width, index) => `minmax(${columnMinimums[index]}px, ${width}fr)`).join(' ');
|
const tableGridTemplate = columnWidths.map((width, index) => `minmax(${columnMinimums[index]}px, ${width}fr)`).join(' ');
|
||||||
@@ -95,19 +97,21 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const revealDownloadFile = async (item: DownloadItem) => {
|
const revealDownloadFile = async (item: DownloadItem) => {
|
||||||
if (item.status !== 'completed') {
|
let pathToReveal: string | null = null;
|
||||||
openProperties(item.id);
|
if (item.status === 'completed') {
|
||||||
return;
|
pathToReveal = await getDownloadPath(item);
|
||||||
|
} else {
|
||||||
|
const settings = useSettingsStore.getState();
|
||||||
|
pathToReveal = item.destination || await resolveCategoryDestination(settings, item.category);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = await getDownloadPath(item);
|
if (!pathToReveal) {
|
||||||
if (!fullPath) {
|
|
||||||
openProperties(item.id);
|
openProperties(item.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invoke('reveal_in_file_manager', { path: fullPath });
|
await invoke('reveal_in_file_manager', { path: pathToReveal });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to show in Finder:", error);
|
console.error("Failed to show in Finder:", error);
|
||||||
showInteractionError('Could not show download in Finder', error);
|
showInteractionError('Could not show download in Finder', error);
|
||||||
@@ -135,6 +139,44 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
default: return d.category === filter;
|
default: return d.category === filter;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const handleItemClick = (e: React.MouseEvent, item: DownloadItem) => {
|
||||||
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
const newSelected = new Set(selectedIds);
|
||||||
|
if (newSelected.has(item.id)) {
|
||||||
|
newSelected.delete(item.id);
|
||||||
|
} else {
|
||||||
|
newSelected.add(item.id);
|
||||||
|
}
|
||||||
|
setSelectedIds(newSelected);
|
||||||
|
setLastSelectedId(item.id);
|
||||||
|
} else if (e.shiftKey && lastSelectedId) {
|
||||||
|
const currentIndex = filteredDownloads.findIndex(d => d.id === item.id);
|
||||||
|
const lastIndex = filteredDownloads.findIndex(d => d.id === lastSelectedId);
|
||||||
|
|
||||||
|
if (currentIndex !== -1 && lastIndex !== -1) {
|
||||||
|
const start = Math.min(currentIndex, lastIndex);
|
||||||
|
const end = Math.max(currentIndex, lastIndex);
|
||||||
|
|
||||||
|
const newSelected = new Set(selectedIds);
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
newSelected.add(filteredDownloads[i].id);
|
||||||
|
}
|
||||||
|
setSelectedIds(newSelected);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSelectedIds(new Set([item.id]));
|
||||||
|
setLastSelectedId(item.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenu = (menu: { x: number; y: number; id: string }) => {
|
||||||
|
if (!selectedIds.has(menu.id)) {
|
||||||
|
setSelectedIds(new Set([menu.id]));
|
||||||
|
setLastSelectedId(menu.id);
|
||||||
|
}
|
||||||
|
setContextMenu(menu);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const getFilterTitle = () => {
|
const getFilterTitle = () => {
|
||||||
if (filter.startsWith('queue:')) {
|
if (filter.startsWith('queue:')) {
|
||||||
@@ -163,8 +205,8 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
useDownloadStore.getState().resumeDownload(item.id);
|
useDownloadStore.getState().resumeDownload(item.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (ids: string | string[]) => {
|
||||||
openDeleteModal(id);
|
openDeleteModal(ids);
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextItem = contextMenu ? downloads.find(d => d.id === contextMenu.id) : null;
|
const contextItem = contextMenu ? downloads.find(d => d.id === contextMenu.id) : null;
|
||||||
@@ -277,11 +319,13 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
downloadId={d.id}
|
downloadId={d.id}
|
||||||
index={index}
|
index={index}
|
||||||
tableGridTemplate={tableGridTemplate}
|
tableGridTemplate={tableGridTemplate}
|
||||||
setContextMenu={setContextMenu}
|
setContextMenu={handleContextMenu}
|
||||||
handlePause={handlePause}
|
handlePause={handlePause}
|
||||||
handleResume={handleResume}
|
handleResume={handleResume}
|
||||||
handleDoubleClick={handleDownloadDoubleClick}
|
handleDoubleClick={handleDownloadDoubleClick}
|
||||||
getCategoryIcon={getCategoryIcon}
|
getCategoryIcon={getCategoryIcon}
|
||||||
|
isSelected={selectedIds.has(d.id)}
|
||||||
|
onClick={handleItemClick}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{Array.from({ length: Math.max(0, 50 - filteredDownloads.length) }).map((_, i) => {
|
{Array.from({ length: Math.max(0, 50 - filteredDownloads.length) }).map((_, i) => {
|
||||||
@@ -311,128 +355,224 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{contextItem.status === 'completed' && (
|
{selectedIds.size > 1 ? (
|
||||||
<button
|
<>
|
||||||
onClick={async () => {
|
{/* Multi-Select Context Menu */}
|
||||||
setContextMenu(null);
|
<button
|
||||||
await openDownloadFile(contextItem);
|
onClick={() => {
|
||||||
}}
|
setContextMenu(null);
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
Array.from(selectedIds).forEach(id => {
|
||||||
>
|
const item = downloads.find(d => d.id === id);
|
||||||
Open
|
if (item && (item.status === 'ready' || item.status === 'paused' || item.status === 'failed' || item.status === 'retrying')) {
|
||||||
</button>
|
handleResume(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Start/Resume
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||||
|
|
||||||
|
<div className="group relative">
|
||||||
|
<button className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors flex justify-between items-center">
|
||||||
|
Add to Queue
|
||||||
|
<ChevronRight size={14} />
|
||||||
|
</button>
|
||||||
|
<div className="absolute left-full top-0 hidden group-hover:block ml-1 min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
|
||||||
|
{queues.map(q => (
|
||||||
|
<button key={q.id} onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
Array.from(selectedIds).forEach(id => {
|
||||||
|
const item = downloads.find(d => d.id === id);
|
||||||
|
if (item && item.status !== 'completed') {
|
||||||
|
updateDownload(id, { queueId: q.id });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}} className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors text-[12px]">
|
||||||
|
{q.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
const urls = Array.from(selectedIds)
|
||||||
|
.map(id => downloads.find(d => d.id === id)?.url)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
navigator.clipboard.writeText(urls).catch(error => {
|
||||||
|
showInteractionError('Could not copy addresses', error);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Copy Address
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
handleDelete(Array.from(selectedIds));
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 text-red-400 hover:bg-red-500/10 transition-colors"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Single-Select Context Menu */}
|
||||||
|
{contextItem.status === 'completed' && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
setContextMenu(null);
|
||||||
|
await openDownloadFile(contextItem);
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
setContextMenu(null);
|
||||||
|
await revealDownloadFile(contextItem);
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Show in Finder
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||||
|
|
||||||
|
{(contextItem.status === 'downloading' || contextItem.status === 'queued' || contextItem.status === 'retrying') && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
handlePause(contextItem.id);
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Pause
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(contextItem.status === 'ready' || contextItem.status === 'paused' || contextItem.status === 'failed' || contextItem.status === 'retrying') && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
handleResume(contextItem);
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
{contextItem.status === 'ready' ? 'Start' : 'Resume'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{['completed', 'failed', 'paused'].includes(contextItem.status) && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
setContextMenu(null);
|
||||||
|
try {
|
||||||
|
await redownload(contextItem.id);
|
||||||
|
} catch (error) {
|
||||||
|
showInteractionError('Redownload failed', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Redownload
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contextItem.status !== 'completed' && (
|
||||||
|
<div className="group relative">
|
||||||
|
<button className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors flex justify-between items-center">
|
||||||
|
Add to Queue
|
||||||
|
<ChevronRight size={14} />
|
||||||
|
</button>
|
||||||
|
<div className="absolute left-full top-0 hidden group-hover:block ml-1 min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
|
||||||
|
{queues.map(q => (
|
||||||
|
<button key={q.id} onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
updateDownload(contextItem.id, { queueId: q.id });
|
||||||
|
}} className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors text-[12px]">
|
||||||
|
{q.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
navigator.clipboard.writeText(contextItem.url).catch(error => {
|
||||||
|
showInteractionError('Could not copy address', error);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Copy Address
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{contextItem.status === 'completed' && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
setContextMenu(null);
|
||||||
|
const fullPath = await getDownloadPath(contextItem);
|
||||||
|
if (!fullPath) {
|
||||||
|
showInteractionError('Could not copy file path', 'File name is missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(fullPath);
|
||||||
|
} catch (error) {
|
||||||
|
showInteractionError('Could not copy file path', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Copy File Path
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
handleDelete(contextItem.id);
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 text-red-400 hover:bg-red-500/10 transition-colors"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
openProperties(contextItem.id);
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
|
>
|
||||||
|
Properties
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
setContextMenu(null);
|
|
||||||
await revealDownloadFile(contextItem);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
|
||||||
>
|
|
||||||
Show in Finder
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
|
||||||
|
|
||||||
{(contextItem.status === 'downloading' || contextItem.status === 'queued' || contextItem.status === 'retrying') && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setContextMenu(null);
|
|
||||||
handlePause(contextItem.id);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
|
||||||
>
|
|
||||||
Pause
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(contextItem.status === 'ready' || contextItem.status === 'paused' || contextItem.status === 'failed' || contextItem.status === 'retrying') && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setContextMenu(null);
|
|
||||||
handleResume(contextItem);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
|
||||||
>
|
|
||||||
{contextItem.status === 'ready' ? 'Start' : 'Resume'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{['completed', 'failed', 'paused'].includes(contextItem.status) && (
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
setContextMenu(null);
|
|
||||||
try {
|
|
||||||
await redownload(contextItem.id);
|
|
||||||
} catch (error) {
|
|
||||||
showInteractionError('Redownload failed', error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
|
||||||
>
|
|
||||||
Redownload
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setContextMenu(null);
|
|
||||||
navigator.clipboard.writeText(contextItem.url).catch(error => {
|
|
||||||
showInteractionError('Could not copy address', error);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
|
||||||
>
|
|
||||||
Copy Address
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{contextItem.status === 'completed' && (
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
setContextMenu(null);
|
|
||||||
const fullPath = await getDownloadPath(contextItem);
|
|
||||||
if (!fullPath) {
|
|
||||||
showInteractionError('Could not copy file path', 'File name is missing');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(fullPath);
|
|
||||||
} catch (error) {
|
|
||||||
showInteractionError('Could not copy file path', error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
|
||||||
>
|
|
||||||
Copy File Path
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setContextMenu(null);
|
|
||||||
handleDelete(contextItem.id);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-3 py-2 text-red-400 hover:bg-red-500/10 transition-colors"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setContextMenu(null);
|
|
||||||
openProperties(contextItem.id);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
|
||||||
>
|
|
||||||
Properties
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1500,6 +1500,10 @@
|
|||||||
background: hsl(var(--stripe-bg));
|
background: hsl(var(--stripe-bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-row.is-selected {
|
||||||
|
background: hsl(var(--accent-color) / 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.download-row:hover {
|
.download-row:hover {
|
||||||
background: hsl(var(--item-hover));
|
background: hsl(var(--item-hover));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export type DownloadDraft = Omit<DownloadItem, 'status' | 'queueId' | 'hasBeenDi
|
|||||||
|
|
||||||
export type DeleteModalState = {
|
export type DeleteModalState = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
downloadId?: string;
|
downloadIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DownloadState {
|
interface DownloadState {
|
||||||
@@ -187,7 +187,7 @@ interface DownloadState {
|
|||||||
) => void;
|
) => void;
|
||||||
handleExtensionDownload: (request: ExtensionDownloadRequest) => void;
|
handleExtensionDownload: (request: ExtensionDownloadRequest) => void;
|
||||||
deleteModalState: DeleteModalState;
|
deleteModalState: DeleteModalState;
|
||||||
openDeleteModal: (downloadId?: string) => void;
|
openDeleteModal: (downloadIds?: string | string[]) => void;
|
||||||
closeDeleteModal: () => void;
|
closeDeleteModal: () => void;
|
||||||
setSelectedPropertiesDownloadId: (id: string | null) => void;
|
setSelectedPropertiesDownloadId: (id: string | null) => void;
|
||||||
addDownload: (item: DownloadDraft, action: AddDownloadAction) => Promise<void>;
|
addDownload: (item: DownloadDraft, action: AddDownloadAction) => Promise<void>;
|
||||||
@@ -258,7 +258,12 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
activeMetadataUrl: null,
|
activeMetadataUrl: null,
|
||||||
parsingError: null,
|
parsingError: null,
|
||||||
deleteModalState: { isOpen: false },
|
deleteModalState: { isOpen: false },
|
||||||
openDeleteModal: (downloadId) => set({ deleteModalState: { isOpen: true, downloadId } }),
|
openDeleteModal: (downloadIds) => set({
|
||||||
|
deleteModalState: {
|
||||||
|
isOpen: true,
|
||||||
|
downloadIds: Array.isArray(downloadIds) ? downloadIds : (downloadIds ? [downloadIds] : undefined)
|
||||||
|
}
|
||||||
|
}),
|
||||||
closeDeleteModal: () => set({ deleteModalState: { isOpen: false } }),
|
closeDeleteModal: () => set({ deleteModalState: { isOpen: false } }),
|
||||||
toggleAddModal: (isOpen) => set({
|
toggleAddModal: (isOpen) => set({
|
||||||
isAddModalOpen: isOpen,
|
isAddModalOpen: isOpen,
|
||||||
|
|||||||
Reference in New Issue
Block a user