From 1f3fe2df3ddc0a238588abe87e4aa539b9af11f0 Mon Sep 17 00:00:00 2001 From: Valics Lehel Date: Fri, 11 Jul 2025 23:14:07 +0300 Subject: [PATCH] feat: Add file metadata display with view modes - Added file size, permissions (rwx format), and modified date display - Implemented three view modes: simple, compact, and detailed - Added server-side file stats collection in getFileTree - View mode preference persisted in localStorage - Detailed view shows table-like layout with column headers - Compact view shows inline metadata - Simple view maintains original basic tree structure --- server/index.js | 32 +++++- src/components/FileTree.jsx | 214 +++++++++++++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 4 deletions(-) diff --git a/server/index.js b/server/index.js index 3baeed8..650e216 100755 --- a/server/index.js +++ b/server/index.js @@ -820,6 +820,14 @@ app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../dist/index.html')); }); +// Helper function to convert permissions to rwx format +function permToRwx(perm) { + const r = perm & 4 ? 'r' : '-'; + const w = perm & 2 ? 'w' : '-'; + const x = perm & 1 ? 'x' : '-'; + return r + w + x; +} + async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) { // Using fsPromises from import const items = []; @@ -836,12 +844,34 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = entry.name === 'dist' || entry.name === 'build') continue; + const itemPath = path.join(dirPath, entry.name); const item = { name: entry.name, - path: path.join(dirPath, entry.name), + path: itemPath, type: entry.isDirectory() ? 'directory' : 'file' }; + // Get file stats for additional metadata + try { + const stats = await fsPromises.stat(itemPath); + item.size = stats.size; + item.modified = stats.mtime.toISOString(); + + // Convert permissions to rwx format + const mode = stats.mode; + const ownerPerm = (mode >> 6) & 7; + const groupPerm = (mode >> 3) & 7; + const otherPerm = mode & 7; + item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString(); + item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm); + } catch (statError) { + // If stat fails, provide default values + item.size = 0; + item.modified = null; + item.permissions = '000'; + item.permissionsRwx = '---------'; + } + if (entry.isDirectory() && currentDepth < maxDepth) { // Recursively get subdirectories but limit depth try { diff --git a/src/components/FileTree.jsx b/src/components/FileTree.jsx index c2328db..1048bdb 100755 --- a/src/components/FileTree.jsx +++ b/src/components/FileTree.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { ScrollArea } from './ui/scroll-area'; import { Button } from './ui/button'; -import { Folder, FolderOpen, File, FileText, FileCode } from 'lucide-react'; +import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye } from 'lucide-react'; import { cn } from '../lib/utils'; import CodeEditor from './CodeEditor'; import ImageViewer from './ImageViewer'; @@ -13,6 +13,7 @@ function FileTree({ selectedProject }) { const [expandedDirs, setExpandedDirs] = useState(new Set()); const [selectedFile, setSelectedFile] = useState(null); const [selectedImage, setSelectedImage] = useState(null); + const [viewMode, setViewMode] = useState('detailed'); // 'simple', 'detailed', 'compact' useEffect(() => { if (selectedProject) { @@ -20,6 +21,14 @@ function FileTree({ selectedProject }) { } }, [selectedProject]); + // Load view mode preference from localStorage + useEffect(() => { + const savedViewMode = localStorage.getItem('file-tree-view-mode'); + if (savedViewMode && ['simple', 'detailed', 'compact'].includes(savedViewMode)) { + setViewMode(savedViewMode); + } + }, []); + const fetchFiles = async () => { setLoading(true); try { @@ -52,6 +61,35 @@ function FileTree({ selectedProject }) { setExpandedDirs(newExpanded); }; + // Change view mode and save preference + const changeViewMode = (mode) => { + setViewMode(mode); + localStorage.setItem('file-tree-view-mode', mode); + }; + + // Format file size + const formatFileSize = (bytes) => { + if (!bytes || bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + }; + + // Format date as relative time + const formatRelativeTime = (date) => { + if (!date) return '-'; + const now = new Date(); + const past = new Date(date); + const diffInSeconds = Math.floor((now - past) / 1000); + + if (diffInSeconds < 60) return 'just now'; + if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} min ago`; + if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`; + if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`; + return past.toLocaleDateString(); + }; + const renderFileTree = (items, level = 0) => { return items.map((item) => (
@@ -135,6 +173,129 @@ function FileTree({ selectedProject }) { } }; + // Render detailed view with table-like layout + const renderDetailedView = (items, level = 0) => { + return items.map((item) => ( +
+
{ + if (item.type === 'directory') { + toggleDirectory(item.path); + } else if (isImageFile(item.name)) { + setSelectedImage({ + name: item.name, + path: item.path, + projectPath: selectedProject.path, + projectName: selectedProject.name + }); + } else { + setSelectedFile({ + name: item.name, + path: item.path, + projectPath: selectedProject.path, + projectName: selectedProject.name + }); + } + }} + > +
+ {item.type === 'directory' ? ( + expandedDirs.has(item.path) ? ( + + ) : ( + + ) + ) : ( + getFileIcon(item.name) + )} + + {item.name} + +
+
+ {item.type === 'file' ? formatFileSize(item.size) : '-'} +
+
+ {formatRelativeTime(item.modified)} +
+
+ {item.permissionsRwx || '-'} +
+
+ + {item.type === 'directory' && + expandedDirs.has(item.path) && + item.children && + renderDetailedView(item.children, level + 1)} +
+ )); + }; + + // Render compact view with inline details + const renderCompactView = (items, level = 0) => { + return items.map((item) => ( +
+
{ + if (item.type === 'directory') { + toggleDirectory(item.path); + } else if (isImageFile(item.name)) { + setSelectedImage({ + name: item.name, + path: item.path, + projectPath: selectedProject.path, + projectName: selectedProject.name + }); + } else { + setSelectedFile({ + name: item.name, + path: item.path, + projectPath: selectedProject.path, + projectName: selectedProject.name + }); + } + }} + > +
+ {item.type === 'directory' ? ( + expandedDirs.has(item.path) ? ( + + ) : ( + + ) + ) : ( + getFileIcon(item.name) + )} + + {item.name} + +
+
+ {item.type === 'file' && ( + <> + {formatFileSize(item.size)} + {item.permissionsRwx} + + )} +
+
+ + {item.type === 'directory' && + expandedDirs.has(item.path) && + item.children && + renderCompactView(item.children, level + 1)} +
+ )); + }; + if (loading) { return (
@@ -147,6 +308,51 @@ function FileTree({ selectedProject }) { return (
+ {/* View Mode Toggle */} +
+

Files

+
+ + + +
+
+ + {/* Column Headers for Detailed View */} + {viewMode === 'detailed' && files.length > 0 && ( +
+
+
Name
+
Size
+
Modified
+
Permissions
+
+
+ )} {files.length === 0 ? ( @@ -160,8 +366,10 @@ function FileTree({ selectedProject }) {

) : ( -
- {renderFileTree(files)} +
+ {viewMode === 'simple' && renderFileTree(files)} + {viewMode === 'compact' && renderCompactView(files)} + {viewMode === 'detailed' && renderDetailedView(files)}
)}