refac
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { getContext, tick, onDestroy } from 'svelte';
|
||||
import { formatFileSize } from '$lib/utils';
|
||||
import type { FileEntry } from '$lib/apis/terminal';
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
export let onMove: (source: string, destFolder: string) => void = () => {};
|
||||
export let onRename: (oldPath: string, newName: string) => void = () => {};
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────
|
||||
export let selected: boolean = false;
|
||||
export let selectionMode: boolean = false;
|
||||
export let selectedPaths: Set<string> = new Set();
|
||||
export let onSelect: (entry: FileEntry, event: MouseEvent) => void = () => {};
|
||||
export let onLongPress: () => void = () => {};
|
||||
|
||||
let dragOverFolder = false;
|
||||
|
||||
// ── Rename state ─────────────────────────────────────────────────────
|
||||
@@ -34,7 +41,6 @@
|
||||
renaming = true;
|
||||
await tick();
|
||||
renameInput?.focus();
|
||||
// Select the name without extension for files
|
||||
if (entry.type === 'file') {
|
||||
const dotIdx = entry.name.lastIndexOf('.');
|
||||
renameInput?.setSelectionRange(0, dotIdx > 0 ? dotIdx : entry.name.length);
|
||||
@@ -54,11 +60,69 @@
|
||||
renaming = false;
|
||||
renameValue = '';
|
||||
};
|
||||
|
||||
// ── Long-press for touch selection ───────────────────────────────────
|
||||
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let didLongPress = false;
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (e.pointerType !== 'touch') return;
|
||||
didLongPress = false;
|
||||
longPressTimer = setTimeout(() => {
|
||||
didLongPress = true;
|
||||
onLongPress();
|
||||
onSelect(entry, e as any);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerCancel = () => {
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
if (longPressTimer) clearTimeout(longPressTimer);
|
||||
});
|
||||
|
||||
// ── Click handler ────────────────────────────────────────────────────
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (renaming) return;
|
||||
if (didLongPress) {
|
||||
didLongPress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Modifier click → toggle/range select
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSelect(entry, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// In selection mode (touch) → toggle select
|
||||
if (selectionMode) {
|
||||
onSelect(entry, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal click → open
|
||||
onOpen(entry);
|
||||
};
|
||||
</script>
|
||||
|
||||
<li class="group">
|
||||
<div
|
||||
class="w-full flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition
|
||||
class="w-full flex items-center transition
|
||||
{selected ? 'bg-blue-50 dark:bg-blue-900/20' : 'hover:bg-gray-50 dark:hover:bg-gray-800'}
|
||||
{dragOverFolder
|
||||
? 'bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-400 dark:ring-blue-500 ring-inset'
|
||||
: ''}"
|
||||
@@ -84,11 +148,11 @@
|
||||
dragOverFolder = false;
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
if (data.path) {
|
||||
const destFolder = `${currentPath}${entry.name}/`;
|
||||
// Don't allow dropping a folder onto itself
|
||||
if (data.path + '/' === destFolder || data.path === destFolder) return;
|
||||
onMove(data.path, destFolder);
|
||||
const paths = data.paths || (data.path ? [data.path] : []);
|
||||
const destFolder = `${currentPath}${entry.name}/`;
|
||||
for (const p of paths) {
|
||||
if (p + '/' === destFolder || p === destFolder) continue;
|
||||
onMove(p, destFolder);
|
||||
}
|
||||
} catch {}
|
||||
}}
|
||||
@@ -98,12 +162,26 @@
|
||||
draggable={true}
|
||||
on:dragstart={(e) => {
|
||||
const filePath = `${currentPath}${entry.name}`;
|
||||
// Internal move data
|
||||
e.dataTransfer?.setData(
|
||||
'application/x-terminal-file-move',
|
||||
JSON.stringify({ path: filePath, name: entry.name })
|
||||
);
|
||||
// Keep existing chat-attachment drag for files
|
||||
// If dragging a selected item, drag all selected
|
||||
if (selected && selectedPaths.size > 1) {
|
||||
e.dataTransfer?.setData(
|
||||
'application/x-terminal-file-move',
|
||||
JSON.stringify({ paths: [...selectedPaths] })
|
||||
);
|
||||
// Custom drag ghost showing count
|
||||
const ghost = document.createElement('div');
|
||||
ghost.style.cssText =
|
||||
'position:fixed;top:-1000px;left:-1000px;display:flex;align-items:center;gap:6px;padding:4px 10px;border-radius:8px;background:#374151;color:#fff;font-size:12px;white-space:nowrap;pointer-events:none;';
|
||||
ghost.textContent = `${selectedPaths.size} items`;
|
||||
document.body.appendChild(ghost);
|
||||
e.dataTransfer?.setDragImage(ghost, 0, 0);
|
||||
requestAnimationFrame(() => ghost.remove());
|
||||
} else {
|
||||
e.dataTransfer?.setData(
|
||||
'application/x-terminal-file-move',
|
||||
JSON.stringify({ path: filePath, name: entry.name })
|
||||
);
|
||||
}
|
||||
if (entry.type === 'file') {
|
||||
e.dataTransfer?.setData(
|
||||
'application/x-terminal-file',
|
||||
@@ -116,13 +194,38 @@
|
||||
);
|
||||
}
|
||||
}}
|
||||
on:click={() => {
|
||||
if (!renaming) onOpen(entry);
|
||||
}}
|
||||
on:pointerdown={onPointerDown}
|
||||
on:pointerup={onPointerUp}
|
||||
on:pointercancel={onPointerCancel}
|
||||
on:click={handleClick}
|
||||
on:dblclick|preventDefault|stopPropagation={() => {
|
||||
startRename();
|
||||
}}
|
||||
>
|
||||
{#if selectionMode || selected}
|
||||
<!-- Checkbox indicator -->
|
||||
<div
|
||||
class="size-3.5 shrink-0 rounded border transition-colors flex items-center justify-center
|
||||
{selected
|
||||
? 'bg-blue-500 dark:bg-blue-600 border-blue-500 dark:border-blue-600 text-white'
|
||||
: 'border-gray-300 dark:border-gray-600'}"
|
||||
>
|
||||
{#if selected}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-2.5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if entry.type === 'directory'}
|
||||
<Folder className="size-4 shrink-0 text-blue-400 dark:text-blue-300" />
|
||||
{:else}
|
||||
@@ -148,8 +251,14 @@
|
||||
bind:value={renameValue}
|
||||
class="flex-1 text-xs bg-transparent border border-gray-200 dark:border-gray-700 rounded px-1.5 py-0.5 outline-none focus:border-blue-400 dark:focus:border-blue-500 text-gray-800 dark:text-gray-200 min-w-0"
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); submitRename(); }
|
||||
if (e.key === 'Escape') { e.preventDefault(); cancelRename(); }
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
submitRename();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cancelRename();
|
||||
}
|
||||
}}
|
||||
on:blur={submitRename}
|
||||
on:click|stopPropagation
|
||||
|
||||
Reference in New Issue
Block a user