This commit is contained in:
Timothy Jaeryang Baek
2026-08-14 21:44:13 -06:00
parent d02b6a21fc
commit 516cf1a9a6
3 changed files with 70 additions and 42 deletions
+56 -30
View File
@@ -121,10 +121,7 @@
const normalizePath = (path: string) => path.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
const cleanEntryName = (name: string) =>
normalizePath(name)
.split('/')
.filter(Boolean)
.at(-1) ?? name.replace(/\/+$/, '');
normalizePath(name).split('/').filter(Boolean).at(-1) ?? name.replace(/\/+$/, '');
const normalizeEntries = (items: FileEntry[]) =>
items.map((entry) => ({ ...entry, name: cleanEntryName(entry.name) }));
@@ -498,11 +495,7 @@
}
const pathForHome = hintPath && hintPath !== '/' ? hintPath : cwdPath;
if (
homePath &&
pathForHome &&
(pathForHome === homePath || pathForHome.startsWith(homePath))
) {
if (homePath && pathForHome && (pathForHome === homePath || pathForHome.startsWith(homePath))) {
return { path: homePath, label: 'Home' };
}
@@ -535,11 +528,11 @@
const isDrive = /^[A-Za-z]:$/.test(parts[0] ?? '');
const root = isDrive ? { label: parts[0], path: `${parts[0]}/` } : { label: '/', path: '/' };
return (isDrive ? parts.slice(1) : parts).reduce(
(acc, part) => {
const prev = acc[acc.length - 1];
acc.push({ label: part, path: asDirectoryPath(joinPath(prev.path, part)) });
return acc;
},
(acc, part) => {
const prev = acc[acc.length - 1];
acc.push({ label: part, path: asDirectoryPath(joinPath(prev.path, part)) });
return acc;
},
[root]
);
};
@@ -571,7 +564,10 @@
};
// ── Directory operations ─────────────────────────────────────────────
const loadDir = async (path: string, options: { preserveTree?: boolean; restoreTree?: boolean } = {}) => {
const loadDir = async (
path: string,
options: { preserveTree?: boolean; restoreTree?: boolean } = {}
) => {
const terminal = selectedTerminal;
if (!terminal) return;
const directory = clampToFileRoot(path);
@@ -663,7 +659,8 @@
};
const openEntry = async (entry: FileEntry) => {
const fullPath = 'fullPath' in entry ? (entry as BrowserRow).fullPath : entryPath(currentPath, entry);
const fullPath =
'fullPath' in entry ? (entry as BrowserRow).fullPath : entryPath(currentPath, entry);
const parentPath = 'parentPath' in entry ? (entry as BrowserRow).parentPath : currentPath;
if (entry.type === 'directory') {
await loadDir(fullPath);
@@ -819,8 +816,8 @@
if (rawMove) {
try {
const data = JSON.parse(rawMove);
const paths = data.paths || (data.path ? [data.path] : []);
for (const path of paths) await handleMove(path, currentPath);
const paths = (data.paths || (data.path ? [data.path] : [])) as string[];
await handleMovePaths(paths, currentPath);
} catch {}
return;
}
@@ -834,7 +831,7 @@
}
uploading = false;
invalidateTreeCache(currentPath);
await loadDir(currentPath, { preserveTree: true });
await loadDir(currentPath, { preserveTree: true });
};
const handleUploadFiles = async (files: File[]) => {
@@ -926,19 +923,25 @@
};
// ── Move (drag-and-drop) ────────────────────────────────────────────
const handleMove = async (source: string, destFolder: string) => {
const sourceParentPath = (source: string) => {
const cleanSource = normalizePath(source).replace(/\/$/, '');
const index = cleanSource.lastIndexOf('/');
return asDirectoryPath(index >= 0 ? cleanSource.slice(0, index + 1) : currentPath);
};
const moveOne = async (source: string, destFolder: string) => {
const terminal = selectedTerminal;
if (!terminal || !currentWritable) return;
if (!terminal || !currentWritable) return false;
const cleanSource = source.replace(/\/$/, '');
const cleanSource = normalizePath(source).replace(/\/$/, '');
const fileName = cleanSource.split('/').pop() ?? '';
const destination = `${asDirectoryPath(destFolder)}${fileName}`;
const destination = joinPath(destFolder, fileName);
if (!fileName || cleanSource === destination) return;
if (!fileName || cleanSource === destination) return false;
// Prevent moving a folder into itself or its own subtree
const sourceDir = asDirectoryPath(cleanSource);
if (asDirectoryPath(destFolder).startsWith(sourceDir)) return;
if (asDirectoryPath(destFolder).startsWith(sourceDir)) return false;
const result = await moveEntry(
terminal.url,
@@ -949,11 +952,27 @@
);
if ('error' in result) {
toast.error(result.error);
return false;
} else {
toast.success($i18n.t('Moved {{name}}', { name: fileName }));
return true;
}
};
const refreshAfterMove = async (sources: string[], destFolder: string) => {
invalidateTreeCache(currentPath, destFolder, ...sources, ...sources.map(sourceParentPath));
clearSelection();
await refreshBrowser();
};
const handleMovePaths = async (sources: string[], destFolder: string) => {
const movedSources: string[] = [];
for (const source of sources) {
if (await moveOne(source, destFolder)) movedSources.push(source);
}
if (movedSources.length > 0) {
await refreshAfterMove(movedSources, destFolder);
}
invalidateTreeCache(currentPath, destFolder, cleanSource.substring(0, cleanSource.lastIndexOf('/') + 1));
await loadDir(currentPath, { preserveTree: true });
};
// ── Rename ──────────────────────────────────────────────────────────
@@ -1325,7 +1344,7 @@
onNewFile={startNewFile}
onUploadFiles={handleUploadFiles}
onDownloadDir={() => downloadFile(currentPath)}
onMove={handleMove}
onMove={handleMovePaths}
onSort={toggleSort}
onToggleHidden={toggleHidden}
>
@@ -1618,10 +1637,17 @@
selected={selectedEntries.has(entry.fullPath)}
{selectionMode}
selectedPaths={selectedEntries}
onOpen={(row) => openEntry({ ...row, fullPath: entry.fullPath, parentPath: entry.parentPath, depth: entry.depth, rowIndex: entry.rowIndex })}
onOpen={(row) =>
openEntry({
...row,
fullPath: entry.fullPath,
parentPath: entry.parentPath,
depth: entry.depth,
rowIndex: entry.rowIndex
})}
onDownload={downloadFile}
onDelete={requestDelete}
onMove={handleMove}
onMove={handleMovePaths}
onRename={handleRename}
onSelect={handleSelect}
onLongPress={enterSelectionMode}
@@ -24,7 +24,7 @@
export let onOpen: (entry: FileEntry) => void = () => {};
export let onDownload: (path: string) => void = () => {};
export let onDelete: (path: string, name: string) => void = () => {};
export let onMove: (source: string, destFolder: string) => void = () => {};
export let onMove: (sources: string[], destFolder: string) => void | Promise<void> = () => {};
export let onRename: (oldPath: string, newName: string) => void = () => {};
// ── Selection ─────────────────────────────────────────────────────────
@@ -187,7 +187,7 @@
dragOverFolder = false;
clearExpandTimer();
}}
on:drop={(e) => {
on:drop={async (e) => {
if (entry.type !== 'directory') return;
if (!writable) return;
const raw = e.dataTransfer?.getData('application/x-terminal-file-move');
@@ -198,12 +198,12 @@
clearExpandTimer();
try {
const data = JSON.parse(raw);
const paths = data.paths || (data.path ? [data.path] : []);
const paths = (data.paths || (data.path ? [data.path] : [])) as string[];
const destFolder = directoryPath;
for (const p of paths) {
if (p + '/' === destFolder || p === destFolder) continue;
onMove(p, destFolder);
}
await onMove(
paths.filter((p) => p + '/' !== destFolder && p !== destFolder),
destFolder
);
} catch {}
}}
>
@@ -19,7 +19,7 @@
export let onNewFile: () => void = () => {};
export let onUploadFiles: (files: File[]) => void = () => {};
export let onDownloadDir: () => void = () => {};
export let onMove: (source: string, destFolder: string) => void = () => {};
export let onMove: (sources: string[], destFolder: string) => void | Promise<void> = () => {};
export let showHidden = false;
export let onToggleHidden: () => void = () => {};
@@ -50,7 +50,9 @@
});
</script>
<div class="m-0 flex items-center gap-1 px-1 pt-0 pb-1.5 shrink-0 border-b border-gray-50 dark:border-gray-850/30">
<div
class="m-0 flex items-center gap-1 px-1 pt-0 pb-1.5 shrink-0 border-b border-gray-50 dark:border-gray-850/30"
>
<div class="flex shrink-0 items-center gap-0.5 px-1">
<!-- Back -->
<Tooltip content={$i18n.t('Back')}>
@@ -108,7 +110,7 @@
on:dragleave={() => {
if (dragOverCrumb === i) dragOverCrumb = null;
}}
on:drop={(e) => {
on:drop={async (e) => {
if (!writable) return;
const raw = e.dataTransfer?.getData('application/x-terminal-file-move');
if (!raw) return;
@@ -117,8 +119,8 @@
dragOverCrumb = null;
try {
const data = JSON.parse(raw);
const paths = data.paths || (data.path ? [data.path] : []);
for (const p of paths) onMove(p, crumb.path);
const paths = (data.paths || (data.path ? [data.path] : [])) as string[];
await onMove(paths, crumb.path);
} catch {}
}}
>