Implement move menu tree, hover tooltips, and custom icons
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m20s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m20s
This commit is contained in:
parent
3802d978ed
commit
e1f009f273
4 changed files with 229 additions and 65 deletions
|
|
@ -37,6 +37,12 @@ type FileInfo struct {
|
||||||
Checksum string `json:"checksum,omitempty"`
|
Checksum string `json:"checksum,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FolderNode struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Children []*FolderNode `json:"children"`
|
||||||
|
}
|
||||||
|
|
||||||
// ListDirectory returns the contents of a directory.
|
// ListDirectory returns the contents of a directory.
|
||||||
// GET /api/files/*
|
// GET /api/files/*
|
||||||
func (h *FSHandler) ListDirectory(c *fiber.Ctx) error {
|
func (h *FSHandler) ListDirectory(c *fiber.Ctx) error {
|
||||||
|
|
@ -127,6 +133,69 @@ func (h *FSHandler) ListDirectory(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFolderTree returns a hierarchical tree of all folders.
|
||||||
|
// GET /api/files/folders-tree
|
||||||
|
func (h *FSHandler) GetFolderTree(c *fiber.Ctx) error {
|
||||||
|
rootPath := h.Config.StorageDir
|
||||||
|
|
||||||
|
root := &FolderNode{
|
||||||
|
Name: "Home",
|
||||||
|
Path: ".",
|
||||||
|
Children: make([]*FolderNode, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeMap := make(map[string]*FolderNode)
|
||||||
|
nodeMap["."] = root
|
||||||
|
|
||||||
|
filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(rootPath, path)
|
||||||
|
if err != nil || relPath == "." || relPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
base := filepath.Base(relPath)
|
||||||
|
if strings.HasPrefix(base, ".") {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath := filepath.ToSlash(relPath)
|
||||||
|
parentPath := filepath.ToSlash(filepath.Dir(relPath))
|
||||||
|
|
||||||
|
node := &FolderNode{
|
||||||
|
Name: d.Name(),
|
||||||
|
Path: cleanPath,
|
||||||
|
Children: make([]*FolderNode, 0),
|
||||||
|
}
|
||||||
|
nodeMap[cleanPath] = node
|
||||||
|
|
||||||
|
if parentNode, exists := nodeMap[parentPath]; exists {
|
||||||
|
parentNode.Children = append(parentNode.Children, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
var sortNode func(node *FolderNode)
|
||||||
|
sortNode = func(node *FolderNode) {
|
||||||
|
sort.Slice(node.Children, func(i, j int) bool {
|
||||||
|
return strings.ToLower(node.Children[i].Name) < strings.ToLower(node.Children[j].Name)
|
||||||
|
})
|
||||||
|
for _, child := range node.Children {
|
||||||
|
sortNode(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sortNode(root)
|
||||||
|
|
||||||
|
return c.JSON(root)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *FSHandler) getFileInfo(c *fiber.Ctx, absPath string) error {
|
func (h *FSHandler) getFileInfo(c *fiber.Ctx, absPath string) error {
|
||||||
info, err := os.Stat(absPath)
|
info, err := os.Stat(absPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,8 @@ func main() {
|
||||||
protected.Get("/files/download-folder", fsHandler.DownloadFolder)
|
protected.Get("/files/download-folder", fsHandler.DownloadFolder)
|
||||||
protected.Post("/files/upload", fsHandler.Upload)
|
protected.Post("/files/upload", fsHandler.Upload)
|
||||||
|
|
||||||
|
protected.Get("/files/folders-tree", fsHandler.GetFolderTree)
|
||||||
|
|
||||||
// File listing and download (with path jail)
|
// File listing and download (with path jail)
|
||||||
files := protected.Group("/files", middleware.PathJail(cfg.StorageDir))
|
files := protected.Group("/files", middleware.PathJail(cfg.StorageDir))
|
||||||
files.Get("/info/*", fsHandler.GetExtendedFileInfo)
|
files.Get("/info/*", fsHandler.GetExtendedFileInfo)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ 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
|
Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import FilePreview from './FilePreview';
|
import FilePreview from './FilePreview';
|
||||||
|
|
@ -35,6 +35,57 @@ interface FileExplorerProps {
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FolderNode {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
children: FolderNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function FolderTreeNode({
|
||||||
|
node,
|
||||||
|
level = 0,
|
||||||
|
selectedPath,
|
||||||
|
onSelect
|
||||||
|
}: {
|
||||||
|
node: FolderNode;
|
||||||
|
level?: number;
|
||||||
|
selectedPath: string;
|
||||||
|
onSelect: (path: string) => void;
|
||||||
|
}) {
|
||||||
|
const isSelected = selectedPath === node.path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{node.path !== '.' && (
|
||||||
|
<button
|
||||||
|
onClick={() => onSelect(node.path)}
|
||||||
|
className="w-full flex items-center gap-2 p-2.5 rounded-xl transition-colors text-left mb-0.5"
|
||||||
|
style={{
|
||||||
|
backgroundColor: isSelected ? 'var(--color-accent-subtle)' : 'transparent',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
paddingLeft: `${level * 16 + 12}px`
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)' }}
|
||||||
|
onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.backgroundColor = 'transparent' }}
|
||||||
|
>
|
||||||
|
{level > 0 && <CornerDownRight className="w-4 h-4 flex-shrink-0 opacity-50" />}
|
||||||
|
<Folder className="w-5 h-5 flex-shrink-0" style={{ color: 'var(--color-accent)' }} />
|
||||||
|
<span className="text-sm font-medium truncate">{node.name}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{node.children?.map((child) => (
|
||||||
|
<FolderTreeNode
|
||||||
|
key={child.path}
|
||||||
|
node={child}
|
||||||
|
level={node.path === '.' ? 0 : level + 1}
|
||||||
|
selectedPath={selectedPath}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type ViewMode = 'grid' | 'list';
|
type ViewMode = 'grid' | 'list';
|
||||||
type SidebarSection = 'files' | 'trash' | 'storage' | 'settings' | 'shared';
|
type SidebarSection = 'files' | 'trash' | 'storage' | 'settings' | 'shared';
|
||||||
|
|
||||||
|
|
@ -60,6 +111,33 @@ function getFileExtension(name: string, isDir: boolean) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Tooltip({ children, text }: { children: React.ReactNode; text: string }) {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative flex items-center justify-center w-full min-w-0"
|
||||||
|
onMouseEnter={() => setShow(true)}
|
||||||
|
onMouseLeave={() => setShow(false)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<AnimatePresence>
|
||||||
|
{show && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 5 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="absolute bottom-full mb-1.5 px-3 py-1.5 bg-black/90 text-white text-xs font-medium rounded-lg shadow-xl whitespace-nowrap z-50 pointer-events-none border border-white/10"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
const [currentPath, setCurrentPath] = useState(() => {
|
const [currentPath, setCurrentPath] = useState(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|
@ -107,7 +185,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
const [showMoveModal, setShowMoveModal] = useState(false);
|
const [showMoveModal, setShowMoveModal] = useState(false);
|
||||||
const [movingFiles, setMovingFiles] = useState<string[]>([]);
|
const [movingFiles, setMovingFiles] = useState<string[]>([]);
|
||||||
const [moveModalPath, setMoveModalPath] = useState('.');
|
const [moveModalPath, setMoveModalPath] = useState('.');
|
||||||
const [moveFolders, setMoveFolders] = useState<FileItem[]>([]);
|
const [folderTree, setFolderTree] = useState<FolderNode | null>(null);
|
||||||
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
||||||
const [pinnedExpanded, setPinnedExpanded] = useState(false);
|
const [pinnedExpanded, setPinnedExpanded] = useState(false);
|
||||||
const [pinnedOrder, setPinnedOrder] = useState<string[]>([]);
|
const [pinnedOrder, setPinnedOrder] = useState<string[]>([]);
|
||||||
|
|
@ -191,20 +269,20 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
}
|
}
|
||||||
}, [currentPath, activeSection]);
|
}, [currentPath, activeSection]);
|
||||||
|
|
||||||
const loadMoveFolders = useCallback(async (path: string) => {
|
const loadFolderTree = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await api.listDirectory(path);
|
const data = await api.getFolderTree();
|
||||||
setMoveFolders(data.files?.filter((f: FileItem) => f.is_dir && f.name !== '.uploads').sort((a: FileItem, b: FileItem) => a.name.localeCompare(b.name)) || []);
|
setFolderTree(data);
|
||||||
} catch {
|
} catch {
|
||||||
setMoveFolders([]);
|
setFolderTree(null);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showMoveModal) {
|
if (showMoveModal) {
|
||||||
loadMoveFolders(moveModalPath);
|
loadFolderTree();
|
||||||
}
|
}
|
||||||
}, [showMoveModal, moveModalPath, loadMoveFolders]);
|
}, [showMoveModal, loadFolderTree]);
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -685,11 +763,31 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let color = 'var(--color-text-tertiary)';
|
if (['mp3', 'wav', 'flac', 'ogg', 'm4a'].includes(ext)) {
|
||||||
if (['mp3', 'wav', 'flac', 'ogg', 'm4a'].includes(ext)) color = '#22c55e';
|
return (
|
||||||
else if (['zip', 'tar', 'gz', 'rar', '7z'].includes(ext)) color = '#8b5cf6';
|
<div className={`${large ? 'w-20 h-20 rounded-2xl' : 'w-6 h-6 rounded'} flex items-center justify-center shadow-sm text-white bg-[#ec4899]`}>
|
||||||
else if (['js', 'ts', 'py', 'go', 'rs', 'md', 'txt'].includes(ext)) color = '#3b82f6';
|
<MusicIcon className={large ? "w-10 h-10 text-white" : "w-3.5 h-3.5 text-white"} />
|
||||||
return <File className={`${sizeClass} fallback-icon`} style={{ color }} />;
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['js', 'ts', 'jsx', 'tsx', 'py', 'go', 'rs', 'java', 'c', 'cpp', 'html', 'css', 'json'].includes(ext)) {
|
||||||
|
return (
|
||||||
|
<div className={`${large ? 'w-20 h-20 rounded-2xl' : 'w-6 h-6 rounded'} flex items-center justify-center shadow-sm text-white bg-[#f59e0b]`}>
|
||||||
|
<CodeIcon className={large ? "w-10 h-10 text-white" : "w-3.5 h-3.5 text-white"} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['zip', 'tar', 'gz', 'rar', '7z'].includes(ext)) {
|
||||||
|
return (
|
||||||
|
<div className={`${large ? 'w-20 h-20 rounded-2xl' : 'w-6 h-6 rounded'} flex items-center justify-center shadow-sm text-white bg-[#14b8a6]`}>
|
||||||
|
<ArchiveIcon className={large ? "w-10 h-10 text-white" : "w-3.5 h-3.5 text-white"} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <File className={`${sizeClass} fallback-icon`} style={{ color: 'var(--color-text-tertiary)' }} />;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (isMedia && file.checksum && viewMode === 'grid') {
|
if (isMedia && file.checksum && viewMode === 'grid') {
|
||||||
|
|
@ -1273,13 +1371,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<Tooltip text={file.name}>
|
||||||
className="text-xs font-medium text-center w-full truncate"
|
<span
|
||||||
style={{ color: 'var(--color-text-primary)' }}
|
className="text-xs font-medium text-center w-full truncate block"
|
||||||
title={getFileDisplayName(file.name, file.is_dir)}
|
style={{ color: 'var(--color-text-primary)' }}
|
||||||
>
|
>
|
||||||
{getFileDisplayName(file.name, file.is_dir)}
|
{getFileDisplayName(file.name, file.is_dir)}
|
||||||
</span>
|
</span>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -1352,7 +1451,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
style={{ color: 'var(--color-text-primary)', borderBottom: '1px solid var(--color-accent)' }}
|
style={{ color: 'var(--color-text-primary)', borderBottom: '1px solid var(--color-accent)' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm truncate" style={{ color: 'var(--color-text-primary)' }} title={getFileDisplayName(file.name, file.is_dir)}>{getFileDisplayName(file.name, file.is_dir)}</span>
|
<Tooltip text={file.name}>
|
||||||
|
<span className="text-sm truncate block" style={{ color: 'var(--color-text-primary)' }}>{getFileDisplayName(file.name, file.is_dir)}</span>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{file.is_pinned && <Pin className="w-3 h-3 flex-shrink-0" style={{ color: 'var(--color-warning)' }} />}
|
{file.is_pinned && <Pin className="w-3 h-3 flex-shrink-0" style={{ color: 'var(--color-warning)' }} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1721,54 +1822,41 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
<X className="w-5 h-5" style={{ color: 'var(--color-text-secondary)' }} />
|
<X className="w-5 h-5" style={{ color: 'var(--color-text-secondary)' }} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 border-b flex items-center gap-2" style={{ borderColor: 'var(--color-border-subtle)', backgroundColor: 'var(--color-bg-secondary)' }}>
|
<div className="flex-1 overflow-y-auto p-2 min-h-[300px]">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => setMoveModalPath('.')}
|
||||||
if (moveModalPath === '.') return;
|
className="w-full flex items-center gap-2 p-2.5 rounded-xl transition-colors text-left mb-2 sticky top-0 z-10 shadow-sm"
|
||||||
const parts = moveModalPath.split('/');
|
style={{
|
||||||
parts.pop();
|
backgroundColor: moveModalPath === '.' ? 'var(--color-accent-subtle)' : 'var(--color-bg-elevated)',
|
||||||
setMoveModalPath(parts.length === 0 ? '.' : parts.join('/'));
|
color: 'var(--color-text-primary)',
|
||||||
|
border: `1px solid ${moveModalPath === '.' ? 'var(--color-accent)' : 'var(--color-border-subtle)'}`
|
||||||
}}
|
}}
|
||||||
disabled={moveModalPath === '.'}
|
onMouseEnter={(e) => { if (moveModalPath !== '.') e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)' }}
|
||||||
className={`p-1.5 rounded-lg ${moveModalPath === '.' ? 'opacity-50' : 'hover:bg-black/5 transition-colors'}`}
|
onMouseLeave={(e) => { if (moveModalPath !== '.') e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)' }}
|
||||||
>
|
>
|
||||||
<ArrowUp className="w-4 h-4" style={{ color: 'var(--color-text-primary)' }} />
|
<Folder className="w-5 h-5 flex-shrink-0" style={{ color: 'var(--color-accent)' }} />
|
||||||
|
<span className="text-sm font-medium truncate">Home (All Files)</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap" style={{ color: 'var(--color-text-primary)' }}>
|
|
||||||
Home {moveModalPath !== '.' && '> ' + moveModalPath.replace(/\//g, ' > ')}
|
{folderTree ? (
|
||||||
</div>
|
folderTree.children?.length === 0 ? (
|
||||||
</div>
|
<div className="h-full flex items-center justify-center text-sm italic mt-8" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||||
<div className="flex-1 overflow-y-auto p-2 min-h-[200px]">
|
No folders here
|
||||||
{moveModalPath !== '.' && (
|
</div>
|
||||||
<button
|
) : (
|
||||||
onClick={() => setMoveModalPath('.')}
|
folderTree.children?.map(child => (
|
||||||
className="w-full flex items-center gap-3 p-3 rounded-xl transition-colors text-left mb-1"
|
<FolderTreeNode
|
||||||
style={{ color: 'var(--color-text-primary)' }}
|
key={child.path}
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)')}
|
node={child}
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
|
selectedPath={moveModalPath}
|
||||||
>
|
onSelect={setMoveModalPath}
|
||||||
<Folder className="w-5 h-5 flex-shrink-0" style={{ color: 'var(--color-accent)' }} />
|
/>
|
||||||
<span className="text-sm font-medium truncate">Home (All Files)</span>
|
))
|
||||||
</button>
|
)
|
||||||
)}
|
|
||||||
{moveFolders.length === 0 ? (
|
|
||||||
<div className="h-full flex items-center justify-center text-sm italic" style={{ color: 'var(--color-text-tertiary)' }}>
|
|
||||||
No folders here
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
moveFolders.map(folder => (
|
<div className="flex items-center justify-center h-32">
|
||||||
<button
|
<RefreshCw className="w-6 h-6 animate-spin" style={{ color: 'var(--color-text-tertiary)' }} />
|
||||||
key={folder.path}
|
</div>
|
||||||
onClick={() => setMoveModalPath(folder.path)}
|
|
||||||
className="w-full flex items-center gap-3 p-3 rounded-xl transition-colors text-left"
|
|
||||||
style={{ color: 'var(--color-text-primary)' }}
|
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)')}
|
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
||||||
>
|
|
||||||
<Folder className="w-5 h-5 flex-shrink-0" style={{ color: 'var(--color-accent)' }} />
|
|
||||||
<span className="text-sm font-medium truncate">{folder.name}</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 border-t flex justify-end gap-3" style={{ borderColor: 'var(--color-border-subtle)' }}>
|
<div className="p-4 border-t flex justify-end gap-3" style={{ borderColor: 'var(--color-border-subtle)' }}>
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,11 @@ class ApiClient {
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFolderTree() {
|
||||||
|
const res = await this.request('/api/files/folders-tree');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
async getExtendedFileInfo(path: string) {
|
async getExtendedFileInfo(path: string) {
|
||||||
const res = await this.request(`/api/files/info/?path=${encodeURIComponent(path)}`);
|
const res = await this.request(`/api/files/info/?path=${encodeURIComponent(path)}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|
|
||||||
Reference in a new issue