vfs:copy/move/delete

This commit is contained in:
lovebird 2026-04-07 12:45:26 +02:00
parent dd159af703
commit 1a1646dad5
3 changed files with 63 additions and 2 deletions

View File

@ -7,13 +7,18 @@ export interface VfsPathRef {
}
export type VfsCopyConflictMode = 'auto' | 'manual';
export type VfsCopyConflictStrategy = 'error' | 'overwrite' | 'skip' | 'rename';
export type VfsCopyConflictStrategy = 'error' | 'overwrite' | 'skip' | 'rename' | 'if_newer';
export type VfsTransferOperation = 'copy' | 'move' | 'delete';
export interface VfsCopyRequest {
operation?: VfsTransferOperation;
source: VfsPathRef;
destination: VfsPathRef;
destination?: VfsPathRef;
conflictMode?: VfsCopyConflictMode;
conflictStrategy?: VfsCopyConflictStrategy;
includePatterns?: string[];
excludePatterns?: string[];
excludeDefault?: boolean;
}
export interface VfsCopyConflict {

View File

@ -21,6 +21,10 @@ export const useStream = () => {
return context;
};
export const useOptionalStream = () => {
return useContext(StreamContext);
};
interface StreamProviderProps {
children: ReactNode;
url?: string;

View File

@ -24,6 +24,8 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { IMAGE_EXTS, VIDEO_EXTS, CODE_EXTS } from '@/modules/storage/helpers';
import { T } from '@/i18n';
import { useOptionalStream } from '@/contexts/StreamContext';
import type { AppEvent } from '@/types-server';
import { useVfsAdapter } from '@/modules/storage/hooks/useVfsAdapter';
import { useSelection } from '@/modules/storage/hooks/useSelection';
@ -238,6 +240,56 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
}
});
const stream = useOptionalStream();
const refreshTimerRef = useRef<number | null>(null);
const scheduleScopedRefresh = useCallback(() => {
if (refreshTimerRef.current) {
window.clearTimeout(refreshTimerRef.current);
}
refreshTimerRef.current = window.setTimeout(() => {
fetchDir(currentPath || '/');
refreshTimerRef.current = null;
}, 250);
}, [currentPath, fetchDir]);
useEffect(() => {
if (!stream) return;
const normalize = (p?: string) => (p || '').replace(/^\/+|\/+$/g, '');
const unsubscribe = stream.subscribe((event: AppEvent) => {
if (event.kind !== 'system') return;
if (event.type !== 'vfs-copy' && event.type !== 'vfs-index') return;
if (event.type === 'vfs-index') {
const evMount = String(event.data?.mount || '');
const evTarget = normalize(String(event.data?.targetPath || ''));
const here = normalize(currentPath);
if (evMount === mount && (evTarget === '' || here === evTarget || here.startsWith(`${evTarget}/`) || evTarget.startsWith(`${here}/`))) {
scheduleScopedRefresh();
}
return;
}
// vfs-copy
const srcMount = String(event.data?.sourceMount || '');
const dstMount = String(event.data?.destinationMount || '');
const srcPath = normalize(String(event.data?.sourcePath || ''));
const dstPath = normalize(String(event.data?.destinationPath || ''));
const here = normalize(currentPath);
const mountMatches = mount === srcMount || mount === dstMount;
const pathMatches = here === '' || here === srcPath || here === dstPath || srcPath.startsWith(`${here}/`) || dstPath.startsWith(`${here}/`);
if (mountMatches && pathMatches) {
scheduleScopedRefresh();
}
});
return () => {
if (refreshTimerRef.current) {
window.clearTimeout(refreshTimerRef.current);
refreshTimerRef.current = null;
}
unsubscribe();
};
}, [stream, mount, currentPath, scheduleScopedRefresh]);
// ── View Mode & Zoom ─────────────────────────────────────────
const displayNodes = useMemo(() => [...sorted, ...(uploads as INode[])], [sorted, uploads]);