import React, { useMemo, useCallback } from 'react'; import type { INode } from '@/modules/storage/types'; import { getMimeCategory, vfsUrl } from '@/modules/storage/helpers'; export interface UseDefaultActionsProps { mount: string; mountProp: string; pathProp: string; accessToken?: string; // Selection selected: INode[]; sorted: INode[]; canGoUp: boolean; setFocusIdx: (idx: number) => void; setSelected: React.Dispatch>; // Preview lightboxNode: INode | null; setLightboxNode: (n: INode | null) => void; textLightboxNode: INode | null; setTextLightboxNode: (n: INode | null) => void; iframeLightboxNode: INode | null; setIframeLightboxNode: (n: INode | null) => void; openPreview: (node: INode) => void; // Navigation updatePath: (path: string) => void; setPendingFileSelect: (v: string | null) => void; // Refs containerRef: React.RefObject; // Helpers from parent getNode: (idx: number) => INode | null; goUp: () => void; } /** * Resolve a relative link `href` against a `basePath` within the VFS. * Returns `{ dirPath, filename }` — `filename` is empty if the link points to a directory. */ export function resolveRelativeVfsLink(href: string, basePath: string): { dirPath: string; filename: string } | null { try { let bp = basePath; if (!bp.startsWith('/')) bp = '/' + bp; if (bp !== '/' && !bp.endsWith('/')) bp += '/'; const resolved = new URL(href, `http://dummy.local${bp}`); let newPath = decodeURIComponent(resolved.pathname); const parts = newPath.split('/'); const lastPart = parts[parts.length - 1]; let filename = ''; if (lastPart && lastPart.includes('.') && !newPath.endsWith('/')) { filename = lastPart; parts.pop(); newPath = parts.join('/') || '/'; } if (newPath !== '/' && newPath.endsWith('/')) { newPath = newPath.slice(0, -1); } return { dirPath: newPath, filename }; } catch { return null; } } export function useDefaultActions({ mount, mountProp, pathProp, accessToken, selected, sorted, canGoUp, setFocusIdx, setSelected, lightboxNode, setLightboxNode, textLightboxNode, setTextLightboxNode, iframeLightboxNode, setIframeLightboxNode, openPreview, updatePath, setPendingFileSelect, containerRef, getNode, goUp, }: UseDefaultActionsProps) { const selectedFile = selected.length === 1 && getMimeCategory(selected[0]) !== 'dir' ? selected[0] : null; // ── File URL builder ───────────────────────────────────────── const getFileUrl = useCallback((node: INode) => { const tokenParam = accessToken ? `token=${encodeURIComponent(accessToken)}` : ''; const base = vfsUrl('get', mountProp, node.path); let url = tokenParam ? `${base}?${tokenParam}` : base; if (node.mtime) { url += (url.includes('?') ? '&' : '?') + `t=${node.mtime}`; } return url; }, [accessToken, mountProp]); // ── View in new tab ────────────────────────────────────────── const handleView = useCallback(() => { if (selected.length === 1) window.open(getFileUrl(selected[0]), '_blank'); }, [selected, getFileUrl]); // ── Download handler ───────────────────────────────────────── const handleDownload = useCallback(() => { if (selected.length === 0) return; if (selected.length === 1) { const first = selected[0]; const cat = getMimeCategory(first); if (cat === 'dir') { const clean = first.path.replace(/^\/+/, ''); let url = vfsUrl('compress', mount, clean); if (accessToken) url += `?token=${encodeURIComponent(accessToken)}`; window.location.href = url; return; } if (selectedFile) { const clean = selectedFile.path.replace(/^\/+/, ''); let url = vfsUrl('get', mount, clean); if (accessToken) url += `?token=${encodeURIComponent(accessToken)}`; if (selectedFile.mtime) url += (url.includes('?') ? '&' : '?') + `t=${selectedFile.mtime}`; url += (url.includes('?') ? '&' : '?') + 'download=1'; window.location.href = url; } return; } // Multiple selection: batch zip const cleanPath = pathProp.replace(/^\/+/, ''); let url = vfsUrl('compress', mount, cleanPath); if (accessToken) { url += `?token=${encodeURIComponent(accessToken)}`; } url += '&files=' + encodeURIComponent(selected.map(n => n.name).join(',')); window.location.href = url; }, [selected, selectedFile, mount, mountProp, pathProp, accessToken]); // ── Download directory as zip ──────────────────────────────── const handleDownloadDir = useCallback(() => { const cleanPath = pathProp.replace(/^\/+/, ''); let url = vfsUrl('compress', mount, cleanPath); if (accessToken) { url += `?token=${encodeURIComponent(accessToken)}`; } window.location.href = url; }, [mount, pathProp, accessToken]); // ── Lightbox navigation ────────────────────────────────────── const mediaNodes = useMemo( () => sorted.filter(n => { const c = getMimeCategory(n); return c === 'image' || c === 'video'; }), [sorted] ); const lightboxIdx = lightboxNode ? mediaNodes.findIndex(n => n.path === lightboxNode.path) : -1; const lightboxPrev = useCallback(() => { if (lightboxIdx > 0) setLightboxNode(mediaNodes[lightboxIdx - 1]); }, [lightboxIdx, mediaNodes, setLightboxNode]); const lightboxNext = useCallback(() => { if (lightboxIdx < mediaNodes.length - 1) setLightboxNode(mediaNodes[lightboxIdx + 1]); }, [lightboxIdx, mediaNodes, setLightboxNode]); // ── Close lightboxes (restore focus + selection) ───────────── const restoreFocusAfterLightbox = useCallback((node: INode | null, closer: (n: null) => void) => { if (node) { const idx = sorted.indexOf(node); if (idx >= 0) { setFocusIdx(canGoUp ? idx + 1 : idx); setSelected([node]); } } closer(null); setTimeout(() => containerRef.current?.focus(), 0); }, [sorted, canGoUp, setFocusIdx, setSelected, containerRef]); const closeLightbox = useCallback( () => restoreFocusAfterLightbox(lightboxNode, setLightboxNode), [restoreFocusAfterLightbox, lightboxNode, setLightboxNode] ); const closeTextLightbox = useCallback( () => restoreFocusAfterLightbox(textLightboxNode, setTextLightboxNode), [restoreFocusAfterLightbox, textLightboxNode, setTextLightboxNode] ); const closeIframeLightbox = useCallback( () => restoreFocusAfterLightbox(iframeLightboxNode, setIframeLightboxNode), [restoreFocusAfterLightbox, iframeLightboxNode, setIframeLightboxNode] ); // ── Double-click handler ───────────────────────────────────── const handleDoubleClick = useCallback((idx: number) => { const node = getNode(idx); if (!node) { goUp(); return; } if (getMimeCategory(node) === 'dir') { updatePath(node.path || node.name); } else { openPreview(node); } }, [getNode, goUp, updatePath, openPreview]); // ── Unified link-click handler (for readme/file-viewer panes) ─ const handleLinkClick = useCallback((href: string, e: React.MouseEvent, basePath: string) => { const isExternal = href.startsWith('http://') || href.startsWith('https://') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('data:') || href.startsWith('#'); if (isExternal) return; e.preventDefault(); const result = resolveRelativeVfsLink(href, basePath); if (!result) { console.error("Failed to resolve relative link:", href); return; } if (result.filename) { setPendingFileSelect(result.filename); } updatePath(result.dirPath); }, [updatePath, setPendingFileSelect]); return { selectedFile, getFileUrl, handleView, handleDownload, handleDownloadDir, mediaNodes, lightboxIdx, lightboxPrev, lightboxNext, closeLightbox, closeTextLightbox, closeIframeLightbox, handleDoubleClick, handleLinkClick }; }