New context menu, info pane fixes, pinned item syncing, and navigation bug fixes
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m19s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m19s
This commit is contained in:
parent
a52f1d63f7
commit
5200654d0f
5 changed files with 111 additions and 27 deletions
|
|
@ -21,6 +21,7 @@ RUN apk add --no-cache \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
sqlite \
|
sqlite \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
mailcap \
|
||||||
tzdata
|
tzdata
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ func (h *SettingsHandler) UpdateSettings(c *fiber.Ctx) error {
|
||||||
"thumbnail_images": true,
|
"thumbnail_images": true,
|
||||||
"thumbnail_videos": true,
|
"thumbnail_videos": true,
|
||||||
"trash_auto_purge_days": true,
|
"trash_auto_purge_days": true,
|
||||||
|
"pinned_sidebar_expanded": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range body {
|
for key, value := range body {
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
const [searchResults, setSearchResults] = useState<FileItem[] | null>(null);
|
const [searchResults, setSearchResults] = useState<FileItem[] | null>(null);
|
||||||
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
||||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; file: FileItem } | null>(null);
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; file: FileItem } | null>(null);
|
||||||
|
const [bgContextMenu, setBgContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||||
const [renaming, setRenaming] = useState<string | null>(null);
|
const [renaming, setRenaming] = useState<string | null>(null);
|
||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState('');
|
||||||
const [showNewFolder, setShowNewFolder] = useState(false);
|
const [showNewFolder, setShowNewFolder] = useState(false);
|
||||||
|
|
@ -109,6 +110,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
if (s) {
|
if (s) {
|
||||||
if (s.grid_size) setGridSize(s.grid_size);
|
if (s.grid_size) setGridSize(s.grid_size);
|
||||||
if (s.trash_auto_purge_days) setTrashAutoPurgeDays(parseInt(s.trash_auto_purge_days, 10));
|
if (s.trash_auto_purge_days) setTrashAutoPurgeDays(parseInt(s.trash_auto_purge_days, 10));
|
||||||
|
if (s.pinned_sidebar_expanded === 'true') setPinnedExpanded(true);
|
||||||
if (s.pinned_order) {
|
if (s.pinned_order) {
|
||||||
try {
|
try {
|
||||||
setPinnedOrder(JSON.parse(s.pinned_order));
|
setPinnedOrder(JSON.parse(s.pinned_order));
|
||||||
|
|
@ -214,11 +216,12 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
|
|
||||||
// Close context menu on click outside
|
// Close context menu on click outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClick() {
|
const handleClickOutside = () => {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}
|
setBgContextMenu(null);
|
||||||
window.addEventListener('click', handleClick);
|
};
|
||||||
return () => window.removeEventListener('click', handleClick);
|
window.addEventListener('click', handleClickOutside);
|
||||||
|
return () => window.removeEventListener('click', handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function loadPinned() {
|
async function loadPinned() {
|
||||||
|
|
@ -339,6 +342,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBgContextMenu(e: React.MouseEvent) {
|
||||||
|
// Only show if the click was on the container itself, not on a file item
|
||||||
|
if ((e.target as HTMLElement).closest('[data-file-item]')) return;
|
||||||
|
e.preventDefault();
|
||||||
|
setContextMenu(null);
|
||||||
|
setBgContextMenu({ x: e.clientX, y: e.clientY });
|
||||||
|
}
|
||||||
|
|
||||||
function handleContextMenu(e: React.MouseEvent, file: FileItem) {
|
function handleContextMenu(e: React.MouseEvent, file: FileItem) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu({ x: e.clientX, y: e.clientY, file });
|
setContextMenu({ x: e.clientX, y: e.clientY, file });
|
||||||
|
|
@ -724,7 +735,11 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
{/* Pinned Collapsible Section */}
|
{/* Pinned Collapsible Section */}
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPinnedExpanded(!pinnedExpanded)}
|
onClick={() => {
|
||||||
|
const next = !pinnedExpanded;
|
||||||
|
setPinnedExpanded(next);
|
||||||
|
api.updateSettings({ pinned_sidebar_expanded: next ? 'true' : 'false' }).catch(console.error);
|
||||||
|
}}
|
||||||
className="w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 hover:bg-white/5"
|
className="w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 hover:bg-white/5"
|
||||||
style={{ color: 'var(--color-text-secondary)' }}
|
style={{ color: 'var(--color-text-secondary)' }}
|
||||||
>
|
>
|
||||||
|
|
@ -748,6 +763,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
pinnedFiles.map((file) => (
|
pinnedFiles.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
|
data-file-item
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
e.dataTransfer.setData('text/plain', file.path);
|
e.dataTransfer.setData('text/plain', file.path);
|
||||||
|
|
@ -764,7 +780,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
}}
|
}}
|
||||||
className="group flex items-center justify-between px-10 py-2 text-sm font-medium cursor-pointer rounded-lg transition-colors hover:bg-white/5 gap-3"
|
className="group flex items-center justify-between px-10 py-2 text-sm font-medium cursor-pointer rounded-lg transition-colors hover:bg-white/5 gap-3"
|
||||||
style={{ color: 'var(--color-text-secondary)' }}
|
style={{ color: 'var(--color-text-secondary)' }}
|
||||||
onClick={() => navigateTo(file.path)}
|
onClick={() => { setActiveSection('files'); navigateTo(file.path); }}
|
||||||
>
|
>
|
||||||
<div className="w-4 h-4 flex items-center justify-center flex-shrink-0 [&>svg]:w-4 [&>svg]:h-4">
|
<div className="w-4 h-4 flex items-center justify-center flex-shrink-0 [&>svg]:w-4 [&>svg]:h-4">
|
||||||
{getFileIcon(file)}
|
{getFileIcon(file)}
|
||||||
|
|
@ -985,7 +1001,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* File Area */}
|
{/* File Area */}
|
||||||
<div className="flex-1 overflow-y-auto p-6 relative">
|
<div className="flex-1 overflow-y-auto p-6 relative" onContextMenu={activeSection === 'files' ? handleBgContextMenu : undefined}>
|
||||||
{/* Drag and drop overlay */}
|
{/* Drag and drop overlay */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{dragOver && (
|
{dragOver && (
|
||||||
|
|
@ -1079,6 +1095,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
{group.files.map((file, idx) => (
|
{group.files.map((file, idx) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
|
data-file-item
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
||||||
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
||||||
|
|
@ -1170,6 +1187,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
{group.files.map((file, idx) => (
|
{group.files.map((file, idx) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
|
data-file-item
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
||||||
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
||||||
|
|
@ -1399,6 +1417,36 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* === BACKGROUND CONTEXT MENU === */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{bgContextMenu && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
className="fixed z-50 w-48 rounded-xl py-1.5 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
left: bgContextMenu.x,
|
||||||
|
top: bgContextMenu.y,
|
||||||
|
backgroundColor: 'var(--color-bg-elevated)',
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
boxShadow: 'var(--shadow-lg)',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<ContextMenuItem
|
||||||
|
icon={<FolderPlus className="w-4 h-4" />}
|
||||||
|
label="New Folder"
|
||||||
|
onClick={() => {
|
||||||
|
setShowNewFolder(true);
|
||||||
|
setBgContextMenu(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Upload Progress Panel */}
|
{/* Upload Progress Panel */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{uploads.length > 0 && (
|
{uploads.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { X, File, Folder, HardDrive, Calendar, RefreshCw } from 'lucide-react';
|
import { X, File, Folder, HardDrive, Calendar, RefreshCw, FileType } from 'lucide-react';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
|
|
||||||
interface FileInfoModalProps {
|
interface FileInfoModalProps {
|
||||||
|
|
@ -10,6 +10,30 @@ interface FileInfoModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHumanFileType(name: string, mimeType?: string): string {
|
||||||
|
if (mimeType && mimeType !== 'application/octet-stream') return mimeType;
|
||||||
|
const ext = name.includes('.') ? name.split('.').pop()?.toLowerCase() : '';
|
||||||
|
if (!ext) return 'Unknown';
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
jpg: 'JPEG Image', jpeg: 'JPEG Image', png: 'PNG Image', gif: 'GIF Image',
|
||||||
|
webp: 'WebP Image', svg: 'SVG Image', bmp: 'BMP Image', ico: 'Icon',
|
||||||
|
mp4: 'MP4 Video', mkv: 'MKV Video', avi: 'AVI Video', mov: 'QuickTime Video',
|
||||||
|
webm: 'WebM Video', flv: 'Flash Video',
|
||||||
|
mp3: 'MP3 Audio', wav: 'WAV Audio', flac: 'FLAC Audio', aac: 'AAC Audio',
|
||||||
|
ogg: 'Ogg Audio', m4a: 'M4A Audio',
|
||||||
|
pdf: 'PDF Document', doc: 'Word Document', docx: 'Word Document',
|
||||||
|
xls: 'Excel Spreadsheet', xlsx: 'Excel Spreadsheet',
|
||||||
|
ppt: 'PowerPoint', pptx: 'PowerPoint',
|
||||||
|
txt: 'Text File', md: 'Markdown', csv: 'CSV File', json: 'JSON File',
|
||||||
|
xml: 'XML File', html: 'HTML File', css: 'CSS File', js: 'JavaScript',
|
||||||
|
ts: 'TypeScript', py: 'Python', go: 'Go', java: 'Java', c: 'C', cpp: 'C++',
|
||||||
|
zip: 'ZIP Archive', rar: 'RAR Archive', '7z': '7-Zip Archive',
|
||||||
|
tar: 'Tar Archive', gz: 'GZip Archive',
|
||||||
|
exe: 'Executable', dmg: 'Disk Image', iso: 'ISO Image',
|
||||||
|
};
|
||||||
|
return map[ext] || `${ext.toUpperCase()} File`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function FileInfoModal({ filePath, onClose }: FileInfoModalProps) {
|
export default function FileInfoModal({ filePath, onClose }: FileInfoModalProps) {
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -68,7 +92,7 @@ export default function FileInfoModal({ filePath, onClose }: FileInfoModalProps)
|
||||||
{data.info.name}
|
{data.info.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs truncate" style={{ color: 'var(--color-text-secondary)' }}>
|
<p className="text-xs truncate" style={{ color: 'var(--color-text-secondary)' }}>
|
||||||
{data.info.is_dir ? 'Folder' : data.info.mime_type || 'Unknown Type'}
|
{data.info.is_dir ? 'Folder' : getHumanFileType(data.info.name, data.info.mime_type)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -90,6 +114,15 @@ export default function FileInfoModal({ filePath, onClose }: FileInfoModalProps)
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!data.info.is_dir && (
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<div className="flex items-center gap-2" style={{ color: 'var(--color-text-secondary)' }}>
|
||||||
|
<FileType className="w-4 h-4" /> Type
|
||||||
|
</div>
|
||||||
|
<span style={{ color: 'var(--color-text-primary)' }}>{getHumanFileType(data.info.name, data.info.mime_type)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center gap-2" style={{ color: 'var(--color-text-secondary)' }}>
|
<div className="flex items-center gap-2" style={{ color: 'var(--color-text-secondary)' }}>
|
||||||
<Calendar className="w-4 h-4" /> Modified
|
<Calendar className="w-4 h-4" /> Modified
|
||||||
|
|
@ -117,3 +150,4 @@ export default function FileInfoModal({ filePath, onClose }: FileInfoModalProps)
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getExtendedFileInfo(path: string) {
|
async getExtendedFileInfo(path: string) {
|
||||||
const res = await this.request(`/api/files/info/${encodeURIComponent(path)}`);
|
const res = await this.request(`/api/files/info/?path=${encodeURIComponent(path)}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue