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