Add password toggle, hover tooltips, and silent refresh for actions
All checks were successful
Automated Container Build / build-and-push (push) Successful in 37s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 37s
This commit is contained in:
parent
0ca098e713
commit
807775a176
2 changed files with 29 additions and 73 deletions
|
|
@ -297,17 +297,16 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
}
|
||||
}
|
||||
|
||||
// Load files on path change
|
||||
const loadFiles = useCallback(async (path?: string) => {
|
||||
const loadFiles = useCallback(async (path?: string, silent: boolean = false) => {
|
||||
const targetPath = path ?? currentPath;
|
||||
setLoading(true);
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
const data = await api.listDirectory(targetPath);
|
||||
setFiles(data.files || []);
|
||||
} catch {
|
||||
setFiles([]);
|
||||
if (!silent) setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
}, [currentPath]);
|
||||
|
||||
|
|
@ -332,10 +331,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
}
|
||||
}, [showMoveModal, loadFolderTree]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Don't trigger shortcuts when typing in inputs
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||
|
||||
if (e.key === 'Backspace') {
|
||||
|
|
@ -357,7 +354,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [files, selectedFiles]);
|
||||
|
||||
// Close context menu on click outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = () => {
|
||||
setContextMenu(null);
|
||||
|
|
@ -367,18 +363,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
return () => window.removeEventListener('click', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
async function loadPinned() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await api.listPinned();
|
||||
setFiles(data.items || []);
|
||||
} catch {
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrash() {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
|
@ -409,12 +393,10 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
}
|
||||
}
|
||||
|
||||
// Effect to keep pinned list loaded for the sidebar
|
||||
const [pinnedFiles, setPinnedFiles] = useState<FileItem[]>([]);
|
||||
useEffect(() => {
|
||||
if (pinnedExpanded) {
|
||||
api.listPinned().then(data => {
|
||||
// Sort by pinnedOrder
|
||||
const items = data.items || [];
|
||||
items.sort((a: FileItem, b: FileItem) => {
|
||||
const idxA = pinnedOrder.indexOf(a.path);
|
||||
|
|
@ -431,7 +413,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
|
||||
async function handlePinnedDragEnd(sourcePath: string, destPath: string) {
|
||||
const newOrder = [...pinnedOrder];
|
||||
// Ensure all current pinned files are in order array
|
||||
const currentPaths = pinnedFiles.map(f => f.path);
|
||||
for (const p of currentPaths) {
|
||||
if (!newOrder.includes(p)) newOrder.push(p);
|
||||
|
|
@ -469,7 +450,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
function handleFileClick(file: FileItem, e: React.MouseEvent) {
|
||||
if (renaming === file.path) return;
|
||||
if (e.shiftKey) {
|
||||
// Multi-select with shift
|
||||
setSelectedFiles(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(file.path)) {
|
||||
|
|
@ -480,7 +460,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
return next;
|
||||
});
|
||||
} else {
|
||||
// Single click to open/navigate
|
||||
if (file.is_dir) navigateToFolder(file);
|
||||
else setPreviewFile(file);
|
||||
}
|
||||
|
|
@ -600,21 +579,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
|
||||
async function handlePin(file: FileItem) {
|
||||
await api.togglePin(file.path);
|
||||
loadFiles();
|
||||
if (pinnedExpanded) {
|
||||
api.listPinned().then(data => {
|
||||
const items = data.items || [];
|
||||
items.sort((a: FileItem, b: FileItem) => {
|
||||
const idxA = pinnedOrder.indexOf(a.path);
|
||||
const idxB = pinnedOrder.indexOf(b.path);
|
||||
if (idxA === -1 && idxB === -1) return a.name.localeCompare(b.name);
|
||||
if (idxA === -1) return 1;
|
||||
if (idxB === -1) return -1;
|
||||
return idxA - idxB;
|
||||
});
|
||||
setPinnedFiles(items);
|
||||
}).catch(console.error);
|
||||
}
|
||||
loadFiles(undefined, true);
|
||||
}
|
||||
|
||||
async function handleRename(path: string, originalName: string, isDir: boolean) {
|
||||
|
|
@ -633,7 +598,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
await api.rename(path, finalName);
|
||||
setRenaming(null);
|
||||
setNewName('');
|
||||
loadFiles();
|
||||
loadFiles(undefined, true);
|
||||
}
|
||||
|
||||
async function handleCreateFolder() {
|
||||
|
|
@ -642,7 +607,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
await api.createFolder(folderPath);
|
||||
setShowNewFolder(false);
|
||||
setNewFolderName('');
|
||||
loadFiles();
|
||||
loadFiles(undefined, true);
|
||||
}
|
||||
|
||||
async function handleRestore(file: FileItem) {
|
||||
|
|
@ -661,14 +626,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
await Promise.all(movingFiles.map(src => api.move(src, moveModalPath === '.' ? '.' : moveModalPath)));
|
||||
setShowMoveModal(false);
|
||||
setMovingFiles([]);
|
||||
loadFiles();
|
||||
loadFiles(undefined, true);
|
||||
setSelectedFiles(new Set());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and drop upload
|
||||
function handleDragOver(e: React.DragEvent) {
|
||||
if (draggedFile) return;
|
||||
if (!e.dataTransfer.types.includes('Files')) return;
|
||||
|
|
@ -711,7 +675,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
setDraggedFile(null);
|
||||
try {
|
||||
await api.move(sourcePath, file.path);
|
||||
loadFiles();
|
||||
loadFiles(undefined, true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
|
@ -741,7 +705,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
if (draggedFile) return;
|
||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||
await Promise.all(droppedFiles.map(file => uploadFile(file)));
|
||||
loadFiles();
|
||||
loadFiles(undefined, true);
|
||||
}
|
||||
|
||||
async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
|
|
@ -749,10 +713,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
if (!uploadFiles) return;
|
||||
await Promise.all(Array.from(uploadFiles).map(file => uploadFile(file)));
|
||||
e.target.value = '';
|
||||
loadFiles();
|
||||
loadFiles(undefined, true);
|
||||
}
|
||||
|
||||
// Breadcrumb path segments
|
||||
const pathSegments = currentPath === '.' ? [] : currentPath.split('/');
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
|
|
@ -851,7 +814,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
|
||||
let displayFiles = searchResults !== null ? searchResults : files;
|
||||
displayFiles = displayFiles.filter(f => f.name !== '.uploads').sort((a, b) => {
|
||||
// Directories always first regardless of sort type
|
||||
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
||||
|
||||
let cmp = 0;
|
||||
|
|
@ -921,7 +883,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
|
||||
return (
|
||||
<div className="flex h-screen" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
{/* === SIDEBAR === */}
|
||||
<aside
|
||||
className="w-60 flex-shrink-0 flex flex-col border-r"
|
||||
style={{
|
||||
|
|
@ -929,7 +890,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
borderColor: 'var(--color-border-subtle)',
|
||||
}}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 px-5 py-5">
|
||||
<div
|
||||
className="w-9 h-9 rounded-xl flex items-center justify-center"
|
||||
|
|
@ -944,7 +904,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-2 space-y-1 overflow-y-auto">
|
||||
{[
|
||||
{ id: 'files' as SidebarSection, icon: Folder, label: 'All Files' },
|
||||
|
|
@ -964,7 +923,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</button>
|
||||
))}
|
||||
|
||||
{/* Pinned Collapsible Section */}
|
||||
<div className="py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -1055,7 +1013,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
))}
|
||||
</nav>
|
||||
|
||||
{/* Settings Footer */}
|
||||
<div className="px-3 py-4 border-t" style={{ borderColor: 'var(--color-border-subtle)' }}>
|
||||
<button
|
||||
id="nav-settings"
|
||||
|
|
@ -1072,14 +1029,12 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</div>
|
||||
</aside>
|
||||
|
||||
{/* === MAIN CONTENT === */}
|
||||
<main
|
||||
className="flex-1 flex flex-col overflow-hidden"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Top Bar */}
|
||||
<header
|
||||
className="flex items-center gap-4 px-6 py-3.5 border-b"
|
||||
style={{
|
||||
|
|
@ -1087,7 +1042,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
borderColor: 'var(--color-border-subtle)',
|
||||
}}
|
||||
>
|
||||
{/* Breadcrumbs */}
|
||||
<div className="flex items-center gap-1 flex-1 min-w-0">
|
||||
<button
|
||||
onClick={() => { navigateTo('.'); setActiveSection('files'); }}
|
||||
|
|
@ -1112,7 +1066,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
{activeSection !== 'settings' && (
|
||||
<div className="relative w-72">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
|
|
@ -1140,7 +1093,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{activeSection === 'trash' && (
|
||||
<button
|
||||
|
|
@ -1231,7 +1183,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</div>
|
||||
</header>
|
||||
|
||||
{/* Dynamic Content Area */}
|
||||
{activeSection === 'settings' ? (
|
||||
<SettingsPage onLogout={onLogout} />
|
||||
) : activeSection === 'storage' ? (
|
||||
|
|
@ -1240,7 +1191,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
<SharedFilesPage />
|
||||
) : (
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* File Area */}
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-6 relative"
|
||||
onContextMenu={activeSection === 'files' ? handleBgContextMenu : undefined}
|
||||
|
|
@ -1250,7 +1200,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
onPointerUp={handlePointerUp}
|
||||
onPointerLeave={handlePointerUp}
|
||||
>
|
||||
{/* Lasso selection box */}
|
||||
{selectionBox && (
|
||||
<div
|
||||
className="fixed z-50 pointer-events-none"
|
||||
|
|
@ -1265,7 +1214,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Drag and drop overlay */}
|
||||
<AnimatePresence>
|
||||
{dragOver && (
|
||||
<motion.div
|
||||
|
|
@ -1291,7 +1239,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* New Folder Input */}
|
||||
<AnimatePresence>
|
||||
{showNewFolder && (
|
||||
<motion.div
|
||||
|
|
@ -1346,7 +1293,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
)}
|
||||
</div>
|
||||
) : viewMode === 'grid' ? (
|
||||
/* Grid View */
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
{groupedFiles.map((group, groupIdx) => (
|
||||
|
|
@ -1435,7 +1381,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* List View */
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
{groupedFiles.map((group, groupIdx) => (
|
||||
|
|
@ -1518,7 +1463,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Bulk Actions Bar */}
|
||||
<AnimatePresence>
|
||||
{selectedFiles.size > 0 && (
|
||||
<motion.div
|
||||
|
|
@ -1544,6 +1488,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
<button
|
||||
className="p-2 hover:bg-white/5 rounded-lg transition-colors flex items-center gap-2 text-sm"
|
||||
style={{ color: 'var(--color-text-primary)' }}
|
||||
title="Download Selected"
|
||||
onClick={() => {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
|
|
@ -1569,6 +1514,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
<button
|
||||
className="p-2 hover:bg-white/5 rounded-lg transition-colors flex items-center gap-2 text-sm"
|
||||
style={{ color: 'var(--color-text-primary)' }}
|
||||
title="Move Selected"
|
||||
onClick={() => {
|
||||
setMovingFiles(Array.from(selectedFiles));
|
||||
setMoveModalPath('.');
|
||||
|
|
@ -1579,13 +1525,14 @@ 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"
|
||||
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();
|
||||
loadFiles(undefined, true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -1597,6 +1544,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
onClick={() => setSelectedFiles(new Set())}
|
||||
className="p-2 hover:bg-white/5 rounded-lg transition-colors"
|
||||
style={{ color: 'var(--color-text-tertiary)' }}
|
||||
title="Clear Selection"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
|
@ -1609,7 +1557,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
)}
|
||||
</main>
|
||||
|
||||
{/* === CONTEXT MENU === */}
|
||||
<AnimatePresence>
|
||||
{contextMenu && (
|
||||
<motion.div
|
||||
|
|
@ -1692,8 +1639,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
label="Move to Trash"
|
||||
danger
|
||||
onClick={() => {
|
||||
api.deleteFile(contextMenu.file.path).then(() => loadFiles());
|
||||
setContextMenu(null);
|
||||
api.deleteFile(contextMenu.file.path).then(() => loadFiles(undefined, true));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
Reference in a new issue