mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-07 01:44:01 +00:00
fix(ui): harden main window interactions
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
useDownloadStore,
|
||||
getSiteLogin,
|
||||
@@ -154,6 +154,15 @@ export const AddDownloadsModal = () => {
|
||||
return hasExtensionRequestContext ? '' : pendingAddFilename;
|
||||
};
|
||||
|
||||
const closeModalFromDismissAction = useCallback(() => {
|
||||
if (isSubmitting) return;
|
||||
const hasPendingInput = Boolean(
|
||||
urls.trim() || pendingAddUrls.trim() || parsedItems.length || headers.trim() || cookies.trim()
|
||||
);
|
||||
if (hasPendingInput && !window.confirm('Discard this download setup?')) return;
|
||||
toggleAddModal(false);
|
||||
}, [cookies, headers, isSubmitting, parsedItems.length, pendingAddUrls, toggleAddModal, urls]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAddModalOpen) {
|
||||
modalSessionRef.current = false;
|
||||
@@ -233,18 +242,20 @@ export const AddDownloadsModal = () => {
|
||||
}, [isQueueMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isQueueMenuOpen && !showingDuplicates) return;
|
||||
if (!isAddModalOpen) return;
|
||||
const closeOnEscape = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
if (showingDuplicates) {
|
||||
setShowingDuplicates(false);
|
||||
} else {
|
||||
} else if (isQueueMenuOpen) {
|
||||
setIsQueueMenuOpen(false);
|
||||
} else {
|
||||
closeModalFromDismissAction();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', closeOnEscape);
|
||||
return () => window.removeEventListener('keydown', closeOnEscape);
|
||||
}, [isQueueMenuOpen, showingDuplicates]);
|
||||
}, [closeModalFromDismissAction, isAddModalOpen, isQueueMenuOpen, showingDuplicates]);
|
||||
|
||||
useEffect(() => {
|
||||
const requestId = ++freeSpaceRequestRef.current;
|
||||
@@ -859,7 +870,14 @@ export const AddDownloadsModal = () => {
|
||||
onCancel={() => setShowingDuplicates(false)}
|
||||
/>
|
||||
)}
|
||||
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div
|
||||
className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) closeModalFromDismissAction();
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className="app-modal add-download-modal flex flex-col overflow-hidden text-sm">
|
||||
|
||||
{/* Main Content Split */}
|
||||
|
||||
@@ -14,6 +14,15 @@ export const DeleteConfirmationModal: React.FC = () => {
|
||||
}
|
||||
}, [deleteModalState.isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!deleteModalState.isOpen || isRemoving) return;
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') closeDeleteModal();
|
||||
};
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
return () => window.removeEventListener('keydown', handleEscape);
|
||||
}, [closeDeleteModal, deleteModalState.isOpen, isRemoving]);
|
||||
|
||||
if (!deleteModalState.isOpen) return null;
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -53,7 +62,14 @@ export const DeleteConfirmationModal: React.FC = () => {
|
||||
const handleDeleteFile = () => removeMany(true);
|
||||
|
||||
return (
|
||||
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div
|
||||
className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget && !isRemoving) handleCancel();
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
className="window-safe-modal 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()}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useDownloadStore } from '../store/useDownloadStore';
|
||||
import { useDownloadProgressStore } from '../store/downloadStore';
|
||||
import { useDownloadProgressStore } from '../store/downloadProgressStore';
|
||||
import { Play, Pause, MoreVertical, Clock, ArrowUp, ArrowDown } from 'lucide-react';
|
||||
import type { DownloadItem as DownloadItemType } from '../bindings/DownloadItem';
|
||||
import { canPauseDownload, canStartDownload, startActionLabel } from '../utils/downloadActions';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export type DuplicateReason = { type: 'url', msg: string } | { type: 'file', msg: string };
|
||||
type DuplicateResolution = 'rename' | 'replace' | 'skip';
|
||||
@@ -20,12 +20,27 @@ interface Props {
|
||||
export const DuplicateResolutionModal = ({ conflicts: initialConflicts, onConfirm, onCancel }: Props) => {
|
||||
const [conflicts, setConflicts] = useState<DuplicateConflict[]>(initialConflicts);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') onCancel();
|
||||
};
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
return () => window.removeEventListener('keydown', handleEscape);
|
||||
}, [onCancel]);
|
||||
|
||||
const updateResolution = (id: string, resolution: DuplicateResolution) => {
|
||||
setConflicts(conflicts.map(c => c.id === id ? { ...c, resolution } : c));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-modal-backdrop fixed inset-0 z-[60] flex items-center justify-center">
|
||||
<div
|
||||
className="app-modal-backdrop fixed inset-0 z-[60] flex items-center justify-center"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) onCancel();
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className="app-modal w-[500px] flex flex-col overflow-hidden text-sm">
|
||||
<div className="p-4 border-b border-border-modal flex flex-col gap-2">
|
||||
<h2 className="text-lg font-semibold text-text-primary">Duplicate Downloads Detected</h2>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import { invokeCommand as invoke } from '../ipc';
|
||||
import { KeyRound, ShieldAlert } from 'lucide-react';
|
||||
@@ -11,6 +11,15 @@ export const KeychainPermissionModal: React.FC = () => {
|
||||
const [isGranting, setIsGranting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showKeychainModal || isGranting) return;
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setShowKeychainModal(false);
|
||||
};
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
return () => window.removeEventListener('keydown', handleEscape);
|
||||
}, [isGranting, setShowKeychainModal, showKeychainModal]);
|
||||
|
||||
if (!showKeychainModal) {
|
||||
return null;
|
||||
}
|
||||
@@ -62,7 +71,14 @@ export const KeychainPermissionModal: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div
|
||||
className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget && !isGranting) handleLater();
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
className="window-safe-modal 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()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useDownloadStore, DownloadItem } from '../store/useDownloadStore';
|
||||
import { useDownloadProgressStore } from '../store/downloadStore';
|
||||
import { useDownloadProgressStore } from '../store/downloadProgressStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import { ChevronDown, ChevronRight, FolderPlus, Info, CheckCircle, AlertCircle, Play, Pause } from 'lucide-react';
|
||||
@@ -114,6 +114,15 @@ export const PropertiesModal = () => {
|
||||
}
|
||||
}, [selectedPropertiesDownloadId, baseDownloadFolder, setSelectedPropertiesDownloadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedPropertiesDownloadId) return;
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setSelectedPropertiesDownloadId(null);
|
||||
};
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
return () => window.removeEventListener('keydown', handleEscape);
|
||||
}, [selectedPropertiesDownloadId, setSelectedPropertiesDownloadId]);
|
||||
|
||||
if (!selectedPropertiesDownloadId || !item) return null;
|
||||
|
||||
const handleBrowse = async () => {
|
||||
@@ -186,7 +195,14 @@ export const PropertiesModal = () => {
|
||||
else if (item.status === 'failed') { statusColor = 'text-red-500'; StatusIcon = AlertCircle; }
|
||||
|
||||
return (
|
||||
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div
|
||||
className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) setSelectedPropertiesDownloadId(null);
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className="app-modal w-[720px] h-[580px] flex flex-col overflow-hidden text-sm">
|
||||
|
||||
{/* Header Summary */}
|
||||
|
||||
@@ -82,6 +82,9 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
const queueId = selectedFilter.replace('queue:', '');
|
||||
const q = queues.find(q => q.id === queueId);
|
||||
if (q && !q.isMain) {
|
||||
if (!window.confirm(`Delete queue "${q.name}"? Its unfinished downloads will move to Main Queue.`)) {
|
||||
return;
|
||||
}
|
||||
void removeQueue(queueId).catch(error => {
|
||||
addToast({ message: `Could not delete queue: ${String(error)}`, variant: 'error', isActionable: true });
|
||||
});
|
||||
@@ -92,7 +95,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedFilter, activeView, queues]);
|
||||
}, [addToast, activeView, queues, removeQueue, selectedFilter]);
|
||||
|
||||
const getCount = (filter: SidebarFilter) => {
|
||||
if (filter.startsWith('queue:')) {
|
||||
@@ -383,6 +386,14 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
className="w-full text-left px-3 py-1.5 flex items-center hover:bg-red-500/20 text-red-400"
|
||||
onClick={() => {
|
||||
const queueId = contextMenu.id;
|
||||
const queue = queues.find(q => q.id === queueId);
|
||||
if (!queue || queue.isMain) {
|
||||
setContextMenu(null);
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`Delete queue "${queue.name}"? Its unfinished downloads will move to Main Queue.`)) {
|
||||
return;
|
||||
}
|
||||
setContextMenu(null);
|
||||
void removeQueue(queueId).catch(error => {
|
||||
addToast({
|
||||
|
||||
Reference in New Issue
Block a user