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,
|
ChevronRight, Grid3X3, List, Plus, Upload, LogOut,
|
||||||
MoreVertical, Star, Download, Pencil, Copy, Move,
|
MoreVertical, Star, Download, Pencil, Copy, Move,
|
||||||
FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon,
|
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';
|
} from 'lucide-react';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
|
|
||||||
|
|
@ -105,6 +106,12 @@ interface UploadProgress {
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Toast {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
type?: 'success' | 'error' | 'info';
|
||||||
|
}
|
||||||
|
|
||||||
function getFileDisplayName(name: string, isDir: boolean) {
|
function getFileDisplayName(name: string, isDir: boolean) {
|
||||||
if (isDir) return name;
|
if (isDir) return name;
|
||||||
const dotIndex = name.lastIndexOf('.');
|
const dotIndex = name.lastIndexOf('.');
|
||||||
|
|
@ -236,6 +243,15 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
const [sortAsc, setSortAsc] = useState(true);
|
const [sortAsc, setSortAsc] = useState(true);
|
||||||
const [draggedFile, setDraggedFile] = useState<string | null>(null);
|
const [draggedFile, setDraggedFile] = useState<string | null>(null);
|
||||||
const [uploads, setUploads] = useState<UploadProgress[]>([]);
|
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 [showTrashModal, setShowTrashModal] = useState(false);
|
||||||
const [showMoveModal, setShowMoveModal] = useState(false);
|
const [showMoveModal, setShowMoveModal] = useState(false);
|
||||||
const [movingFiles, setMovingFiles] = useState<string[]>([]);
|
const [movingFiles, setMovingFiles] = useState<string[]>([]);
|
||||||
|
|
@ -599,17 +615,21 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
|
|
||||||
async function deleteSelected() {
|
async function deleteSelected() {
|
||||||
if (selectedFiles.size === 0) return;
|
if (selectedFiles.size === 0) return;
|
||||||
|
if (!confirm(`Move ${selectedFiles.size} items to trash?`)) return;
|
||||||
|
const count = selectedFiles.size;
|
||||||
for (const path of selectedFiles) {
|
for (const path of selectedFiles) {
|
||||||
await api.deleteFile(path);
|
await api.deleteFile(path);
|
||||||
}
|
}
|
||||||
setSelectedFiles(new Set());
|
setSelectedFiles(new Set());
|
||||||
loadFiles();
|
loadFiles(undefined, true);
|
||||||
|
showToast(`Deleted ${count} item${count > 1 ? 's' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePin(file: FileItem) {
|
async function handlePin(file: FileItem) {
|
||||||
await api.togglePin(file.path);
|
await api.togglePin(file.path);
|
||||||
loadFiles(undefined, true);
|
loadFiles(undefined, true);
|
||||||
setPinnedRefreshCounter(c => c + 1);
|
setPinnedRefreshCounter(c => c + 1);
|
||||||
|
showToast(file.is_pinned ? 'Unpinned item' : 'Pinned item');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRename(path: string, originalName: string, isDir: boolean) {
|
async function handleRename(path: string, originalName: string, isDir: boolean) {
|
||||||
|
|
@ -629,6 +649,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
setRenaming(null);
|
setRenaming(null);
|
||||||
setNewName('');
|
setNewName('');
|
||||||
loadFiles(undefined, true);
|
loadFiles(undefined, true);
|
||||||
|
showToast(`Renamed to ${finalName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateFolder() {
|
async function handleCreateFolder() {
|
||||||
|
|
@ -643,11 +664,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
async function handleRestore(file: FileItem) {
|
async function handleRestore(file: FileItem) {
|
||||||
await api.restoreFromTrash(file.path);
|
await api.restoreFromTrash(file.path);
|
||||||
loadTrash();
|
loadTrash();
|
||||||
|
showToast(`Restored ${file.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEmptyTrash() {
|
async function handleEmptyTrash() {
|
||||||
await api.emptyTrash();
|
await api.emptyTrash();
|
||||||
loadTrash();
|
loadTrash();
|
||||||
|
showToast('Trash emptied');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMoveExecute() {
|
async function handleMoveExecute() {
|
||||||
|
|
@ -658,6 +681,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
setMovingFiles([]);
|
setMovingFiles([]);
|
||||||
loadFiles(undefined, true);
|
loadFiles(undefined, true);
|
||||||
setSelectedFiles(new Set());
|
setSelectedFiles(new Set());
|
||||||
|
showToast(`Moved ${movingFiles.length} item${movingFiles.length > 1 ? 's' : ''}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
@ -706,6 +730,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
try {
|
try {
|
||||||
await api.move(sourcePath, file.path);
|
await api.move(sourcePath, file.path);
|
||||||
loadFiles(undefined, true);
|
loadFiles(undefined, true);
|
||||||
|
showToast('Item moved');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
@ -1553,15 +1578,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-red-500/10 rounded-lg transition-colors flex items-center gap-2 text-sm text-red-500"
|
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"
|
title="Delete Selected"
|
||||||
onClick={async () => {
|
onClick={() => deleteSelected()}
|
||||||
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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1676,8 +1693,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
label="Move to Trash"
|
label="Move to Trash"
|
||||||
danger
|
danger
|
||||||
onClick={() => {
|
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);
|
setContextMenu(null);
|
||||||
api.deleteFile(contextMenu.file.path).then(() => loadFiles(undefined, true));
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
@ -1716,6 +1738,30 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</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 */}
|
{/* Upload Progress Panel */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{uploads.length > 0 && (
|
{uploads.length > 0 && (
|
||||||
|
|
|
||||||
Reference in a new issue