mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-04 00:18:41 +00:00
fix: repair download interaction flows
This commit is contained in:
@@ -269,6 +269,7 @@ export const AddDownloadsModal = () => {
|
||||
const [selectedQueueId, setSelectedQueueId] = useState<string>(MAIN_QUEUE_ID);
|
||||
|
||||
const [urls, setUrls] = useState('');
|
||||
const [metadataRefreshNonce, setMetadataRefreshNonce] = useState(0);
|
||||
const [selectedItemIndex, setSelectedItemIndex] = useState<number | null>(null);
|
||||
const [parsedItems, setParsedItems] = useState<ParsedDownloadItem[]>([]);
|
||||
|
||||
@@ -464,7 +465,7 @@ export const AddDownloadsModal = () => {
|
||||
active = false;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [urls, pendingAddFilename, isSaveLocationManual]);
|
||||
}, [urls, pendingAddFilename, isSaveLocationManual, metadataRefreshNonce]);
|
||||
|
||||
if (!isAddModalOpen) return null;
|
||||
|
||||
@@ -569,6 +570,7 @@ export const AddDownloadsModal = () => {
|
||||
const idx = parseInt(res.id);
|
||||
const item = itemsToAdd[idx];
|
||||
if (!item) continue;
|
||||
const conflict = conflicts.find(c => c.id === res.id);
|
||||
|
||||
if (res.resolution === 'skip') {
|
||||
itemsToAdd[idx] = null;
|
||||
@@ -601,8 +603,12 @@ export const AddDownloadsModal = () => {
|
||||
}
|
||||
|
||||
itemsToAdd[idx] = { ...item, file: newName };
|
||||
} else if (res.resolution === 'replace') {
|
||||
let finalFile = item.file;
|
||||
} else if (res.resolution === 'replace') {
|
||||
if (conflict?.reason.type !== 'file') {
|
||||
itemsToAdd[idx] = null;
|
||||
continue;
|
||||
}
|
||||
let finalFile = item.file;
|
||||
if (item.isMedia && item.formats && item.selectedFormat !== undefined) {
|
||||
const selectedFormat = item.formats[item.selectedFormat];
|
||||
const baseName = finalFile.substring(0, finalFile.lastIndexOf('.')) || finalFile;
|
||||
@@ -734,9 +740,13 @@ export const AddDownloadsModal = () => {
|
||||
/>
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<span className="text-[11px] text-text-muted font-medium">{parsedItems.length} valid link(s) detected</span>
|
||||
<button className="flex items-center gap-1.5 text-[11px] text-blue-500 hover:text-blue-400 font-medium">
|
||||
<RefreshCw size={12} /> Refresh Metadata
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMetadataRefreshNonce(value => value + 1)}
|
||||
className="flex items-center gap-1.5 text-[11px] text-blue-500 hover:text-blue-400 font-medium"
|
||||
>
|
||||
<RefreshCw size={12} /> Refresh Metadata
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useDownloadStore } from '../store/useDownloadStore';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
export const DeleteConfirmationModal: React.FC = () => {
|
||||
const { deleteModalState, closeDeleteModal, removeDownload } = useDownloadStore();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
if (!deleteModalState.isOpen) return null;
|
||||
|
||||
@@ -11,58 +13,71 @@ export const DeleteConfirmationModal: React.FC = () => {
|
||||
closeDeleteModal();
|
||||
};
|
||||
|
||||
const handleRemoveFromList = () => {
|
||||
const handleRemoveFromList = async () => {
|
||||
if (deleteModalState.downloadId) {
|
||||
removeDownload(deleteModalState.downloadId, false);
|
||||
setIsRemoving(true);
|
||||
try {
|
||||
await removeDownload(deleteModalState.downloadId, false);
|
||||
} catch (error) {
|
||||
setErrorMessage(`Remove failed: ${String(error)}`);
|
||||
setIsRemoving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
closeDeleteModal();
|
||||
};
|
||||
|
||||
const handleDeleteFile = () => {
|
||||
const handleDeleteFile = async () => {
|
||||
if (deleteModalState.downloadId) {
|
||||
removeDownload(deleteModalState.downloadId, true);
|
||||
setIsRemoving(true);
|
||||
try {
|
||||
await removeDownload(deleteModalState.downloadId, true);
|
||||
} catch (error) {
|
||||
setErrorMessage(`Delete failed: ${String(error)}`);
|
||||
setIsRemoving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
closeDeleteModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 animate-fade-in">
|
||||
<div
|
||||
<div
|
||||
className="bg-bg-modal rounded-xl w-full max-w-md overflow-hidden flex flex-col shadow-2xl border border-border-modal scale-in"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-5 py-4 border-b border-border-modal flex items-center gap-3">
|
||||
<div className="p-2 bg-red-500/10 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle size={20} className="text-red-400" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-text-primary m-0">
|
||||
Remove Download
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary m-0">Remove Download</h2>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<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.
|
||||
{errorMessage && <div className="mt-3 text-xs text-red-400">{errorMessage}</div>}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-5 py-4 border-t border-border-modal flex justify-end gap-3 bg-bg-modal-accent">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors text-text-secondary hover:bg-item-hover hover:text-text-primary"
|
||||
disabled={isRemoving}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors text-text-secondary hover:bg-item-hover hover:text-text-primary disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemoveFromList}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-border-modal hover:bg-border-modal/80 text-text-primary"
|
||||
disabled={isRemoving}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-border-modal hover:bg-border-modal/80 text-text-primary disabled:opacity-50"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteFile}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-red-500/20 text-red-400 hover:bg-red-500/30"
|
||||
disabled={isRemoving}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-red-500/20 text-red-400 hover:bg-red-500/30 disabled:opacity-50"
|
||||
>
|
||||
Delete file
|
||||
</button>
|
||||
|
||||
@@ -178,7 +178,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
{download.status === 'queued' && queueIndex !== -1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => moveInQueue(download.id, 'Up')}
|
||||
onClick={() => moveInQueue(download.id, 'up')}
|
||||
disabled={queueIndex === 0}
|
||||
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
||||
title="Move Up"
|
||||
@@ -186,7 +186,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
<ArrowUp size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => moveInQueue(download.id, 'Down')}
|
||||
onClick={() => moveInQueue(download.id, 'down')}
|
||||
disabled={queueIndex === pendingOrder.length - 1}
|
||||
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
||||
title="Move Down"
|
||||
|
||||
@@ -390,13 +390,15 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
|
||||
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setContextMenu(null);
|
||||
navigator.clipboard.writeText(contextItem.url);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -404,8 +406,16 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
<button
|
||||
onClick={async () => {
|
||||
setContextMenu(null);
|
||||
const fullPath = await resolvePath(contextItem.destination || '~/Downloads', contextItem.fileName);
|
||||
navigator.clipboard.writeText(fullPath);
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -44,7 +44,7 @@ export const DuplicateResolutionModal = ({ conflicts: initialConflicts, onConfir
|
||||
className="app-control w-24 shrink-0 px-2 py-1 text-xs"
|
||||
>
|
||||
<option value="rename">Rename</option>
|
||||
<option value="replace">Replace</option>
|
||||
{conflict.reason.type === 'file' && <option value="replace">Replace</option>}
|
||||
<option value="skip">Skip</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -190,7 +190,7 @@ export const PropertiesModal = () => {
|
||||
<span>
|
||||
{item.status === 'completed'
|
||||
? "File identity is read-only. Transfer settings are saved for redownload."
|
||||
: "Only the speed limit applies to the current transfer. Other settings can be changed after stopping or pausing."}
|
||||
: "Transfer settings can be changed after stopping or pausing. Current transfers keep their existing backend options."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -222,12 +222,12 @@ export const PropertiesModal = () => {
|
||||
<label className="text-xs text-text-muted text-right">Speed</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 text-xs text-text-primary">
|
||||
<input type="checkbox" checked={speedLimitEnabled} onChange={e => setSpeedLimitEnabled(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20 bg-bg-input" />
|
||||
<input type="checkbox" checked={speedLimitEnabled} onChange={e => setSpeedLimitEnabled(e.target.checked)} disabled={isTransferLocked} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20 bg-bg-input disabled:opacity-50" />
|
||||
Limit
|
||||
</label>
|
||||
{speedLimitEnabled && (
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="number" value={speedLimitValue} min={1} step={128} onChange={e=>setSpeedLimitValue(e.target.value)} className="w-20 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent" />
|
||||
<input type="number" value={speedLimitValue} min={1} step={128} onChange={e=>setSpeedLimitValue(e.target.value)} disabled={isTransferLocked} className="w-20 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" />
|
||||
<span className="text-xs text-text-muted">KiB/s</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user