feat: add toast notifications for file actions
All checks were successful
Automated Container Build / build-and-push (push) Successful in 39s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 39s
This commit is contained in:
parent
cf43dd0e5a
commit
ee36e16dae
1 changed files with 58 additions and 12 deletions
|
|
@ -7,7 +7,8 @@ import {
|
|||
ChevronRight, Grid3X3, List, Plus, Upload, LogOut,
|
||||
MoreVertical, Star, Download, Pencil, Copy, Move,
|
||||
FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon,
|
||||
Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight
|
||||
Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight,
|
||||
CheckCircle, AlertCircle
|
||||
} from 'lucide-react';
|
||||
import api from '@/lib/api';
|
||||
|
||||
|
|
@ -105,6 +106,12 @@ interface UploadProgress {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
interface Toast {
|
||||
id: number;
|
||||
message: string;
|
||||
type?: 'success' | 'error' | 'info';
|
||||
}
|
||||
|
||||
function getFileDisplayName(name: string, isDir: boolean) {
|
||||
if (isDir) return name;
|
||||
const dotIndex = name.lastIndexOf('.');
|
||||
|
|
@ -236,6 +243,15 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
const [sortAsc, setSortAsc] = useState(true);
|
||||
const [draggedFile, setDraggedFile] = useState<string | null>(null);
|
||||
const [uploads, setUploads] = useState<UploadProgress[]>([]);
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
function showToast(message: string, type: 'success' | 'error' | 'info' = 'success') {
|
||||
const id = Date.now() + Math.random();
|
||||
setToasts(prev => [...prev, { id, message, type }]);
|
||||
setTimeout(() => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id));
|
||||
}, 3000);
|
||||
}
|
||||
const [showTrashModal, setShowTrashModal] = useState(false);
|
||||
const [showMoveModal, setShowMoveModal] = useState(false);
|
||||
const [movingFiles, setMovingFiles] = useState<string[]>([]);
|
||||
|
|
@ -599,17 +615,21 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
|
||||
async function deleteSelected() {
|
||||
if (selectedFiles.size === 0) return;
|
||||
if (!confirm(`Move ${selectedFiles.size} items to trash?`)) return;
|
||||
const count = selectedFiles.size;
|
||||
for (const path of selectedFiles) {
|
||||
await api.deleteFile(path);
|
||||
}
|
||||
setSelectedFiles(new Set());
|
||||
loadFiles();
|
||||
loadFiles(undefined, true);
|
||||
showToast(`Deleted ${count} item${count > 1 ? 's' : ''}`);
|
||||
}
|
||||
|
||||
async function handlePin(file: FileItem) {
|
||||
await api.togglePin(file.path);
|
||||
loadFiles(undefined, true);
|
||||
setPinnedRefreshCounter(c => c + 1);
|
||||
showToast(file.is_pinned ? 'Unpinned item' : 'Pinned item');
|
||||
}
|
||||
|
||||
async function handleRename(path: string, originalName: string, isDir: boolean) {
|
||||
|
|
@ -629,6 +649,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
setRenaming(null);
|
||||
setNewName('');
|
||||
loadFiles(undefined, true);
|
||||
showToast(`Renamed to ${finalName}`);
|
||||
}
|
||||
|
||||
async function handleCreateFolder() {
|
||||
|
|
@ -643,11 +664,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
async function handleRestore(file: FileItem) {
|
||||
await api.restoreFromTrash(file.path);
|
||||
loadTrash();
|
||||
showToast(`Restored ${file.name}`);
|
||||
}
|
||||
|
||||
async function handleEmptyTrash() {
|
||||
await api.emptyTrash();
|
||||
loadTrash();
|
||||
showToast('Trash emptied');
|
||||
}
|
||||
|
||||
async function handleMoveExecute() {
|
||||
|
|
@ -658,6 +681,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
setMovingFiles([]);
|
||||
loadFiles(undefined, true);
|
||||
setSelectedFiles(new Set());
|
||||
showToast(`Moved ${movingFiles.length} item${movingFiles.length > 1 ? 's' : ''}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
|
@ -706,6 +730,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
try {
|
||||
await api.move(sourcePath, file.path);
|
||||
loadFiles(undefined, true);
|
||||
showToast('Item moved');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
|
@ -1553,15 +1578,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
<button
|
||||
className="p-2 hover:bg-red-500/10 rounded-lg transition-colors flex items-center gap-2 text-sm text-red-500"
|
||||
title="Delete Selected"
|
||||
onClick={async () => {
|
||||
if (confirm(`Move ${selectedFiles.size} items to trash?`)) {
|
||||
for (const path of Array.from(selectedFiles)) {
|
||||
await api.deleteFile(path).catch(() => {});
|
||||
}
|
||||
setSelectedFiles(new Set());
|
||||
loadFiles(undefined, true);
|
||||
}
|
||||
}}
|
||||
onClick={() => deleteSelected()}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
|
@ -1676,8 +1693,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
label="Move to Trash"
|
||||
danger
|
||||
onClick={() => {
|
||||
if (confirm(`Move ${contextMenu.file.name} to trash?`)) {
|
||||
api.deleteFile(contextMenu.file.path).then(() => {
|
||||
loadFiles(undefined, true);
|
||||
showToast(`Deleted ${contextMenu.file.name}`);
|
||||
});
|
||||
}
|
||||
setContextMenu(null);
|
||||
api.deleteFile(contextMenu.file.path).then(() => loadFiles(undefined, true));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
@ -1716,6 +1738,30 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Toast Notifications */}
|
||||
<div className="fixed bottom-6 right-6 z-[60] flex flex-col gap-2 pointer-events-none">
|
||||
<AnimatePresence>
|
||||
{toasts.map(toast => (
|
||||
<motion.div
|
||||
key={toast.id}
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||
className="px-4 py-3 rounded-xl shadow-xl flex items-center gap-3 backdrop-blur-md border border-white/10 pointer-events-auto"
|
||||
style={{
|
||||
backgroundColor: toast.type === 'error' ? 'rgba(239, 68, 68, 0.9)' : 'var(--color-bg-elevated)',
|
||||
color: toast.type === 'error' ? 'white' : 'var(--color-text-primary)'
|
||||
}}
|
||||
>
|
||||
{toast.type === 'success' && <CheckCircle className="w-5 h-5 text-green-500" />}
|
||||
{toast.type === 'error' && <AlertCircle className="w-5 h-5 text-white" />}
|
||||
{toast.type === 'info' && <Info className="w-5 h-5 text-blue-500" />}
|
||||
<span className="font-medium text-sm">{toast.message}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Upload Progress Panel */}
|
||||
<AnimatePresence>
|
||||
{uploads.length > 0 && (
|
||||
|
|
|
|||
Reference in a new issue