refac
This commit is contained in:
1 parent
5c05608e3a
commit
7dfbdd221a
5 files changed
+632
-447
No files matched your search
@@ -6,16 +6,18 @@
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
||||
import EllipsisHorizontal from '../../icons/EllipsisHorizontal.svelte';
|
||||
import GarbageBin from '../../icons/GarbageBin.svelte';
|
||||
import Pencil from '../../icons/Pencil.svelte';
|
||||
import Clipboard from '../../icons/Clipboard.svelte';
|
||||
import FileTypeIcon from './FileTypeIcon.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
|
||||
export let entry: FileEntry;
|
||||
export let currentPath: string;
|
||||
export let fullPath: string | null = null;
|
||||
export let depth = 0;
|
||||
export let rowIndex = 0;
|
||||
export let expanded = false;
|
||||
export let loadingChildren = false;
|
||||
export let terminalUrl: string = '';
|
||||
export let terminalKey: string = '';
|
||||
|
||||
@@ -29,13 +31,23 @@
|
||||
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 onSelect: (
|
||||
entry: FileEntry,
|
||||
event: MouseEvent,
|
||||
path: string,
|
||||
index: number
|
||||
) => void = () => {};
|
||||
export let onLongPress: () => void = () => {};
|
||||
export let onToggleExpand: (path: string) => void = () => {};
|
||||
export let showDate: boolean = false;
|
||||
export let parentWritable = true;
|
||||
|
||||
$: entryPath =
|
||||
fullPath ?? (entry.type === 'directory' ? `${currentPath}${entry.name}/` : `${currentPath}${entry.name}`);
|
||||
$: directoryPath = entryPath.endsWith('/') ? entryPath : `${entryPath}/`;
|
||||
$: writable = entry.writable !== false;
|
||||
$: canMutate = parentWritable && writable;
|
||||
$: rowIndent = `${12 + depth * 16}px`;
|
||||
|
||||
const formatRelativeTime = (epoch: number): string => {
|
||||
const diff = Math.floor(Date.now() / 1000) - epoch;
|
||||
@@ -48,6 +60,13 @@
|
||||
};
|
||||
|
||||
let dragOverFolder = false;
|
||||
let expandTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clearExpandTimer = () => {
|
||||
if (!expandTimer) return;
|
||||
clearTimeout(expandTimer);
|
||||
expandTimer = null;
|
||||
};
|
||||
|
||||
// ── Rename state ─────────────────────────────────────────────────────
|
||||
let renaming = false;
|
||||
@@ -71,7 +90,7 @@
|
||||
const newName = renameValue.trim();
|
||||
renaming = false;
|
||||
if (!newName || newName === entry.name) return;
|
||||
onRename(`${currentPath}${entry.name}`, newName);
|
||||
onRename(entryPath.replace(/\/$/, ''), newName);
|
||||
};
|
||||
|
||||
const cancelRename = () => {
|
||||
@@ -89,7 +108,7 @@
|
||||
longPressTimer = setTimeout(() => {
|
||||
didLongPress = true;
|
||||
onLongPress();
|
||||
onSelect(entry, e as any);
|
||||
onSelect(entry, e as any, entryPath, rowIndex);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
@@ -109,6 +128,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
if (longPressTimer) clearTimeout(longPressTimer);
|
||||
clearExpandTimer();
|
||||
});
|
||||
|
||||
// ── Click handler ────────────────────────────────────────────────────
|
||||
@@ -122,13 +142,13 @@
|
||||
// Modifier click → toggle/range select
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSelect(entry, e);
|
||||
onSelect(entry, e, entryPath, rowIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// In selection mode (touch) → toggle select
|
||||
if (selectionMode) {
|
||||
onSelect(entry, e);
|
||||
onSelect(entry, e, entryPath, rowIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,14 +157,13 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<li class="group">
|
||||
<li class="group" data-file-row>
|
||||
<div
|
||||
class="w-full flex items-center transition
|
||||
{selected ? 'bg-blue-50 dark:bg-blue-900/20' : 'hover:bg-gray-50/40 dark:hover:bg-gray-850/30'}
|
||||
class="w-full flex items-center transition-colors duration-75
|
||||
{selected ? 'bg-blue-50 dark:bg-blue-500/10' : 'hover:bg-gray-50/40 dark:hover:bg-white/4'}
|
||||
{dragOverFolder
|
||||
? 'bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-400 dark:ring-blue-500 ring-inset'
|
||||
? 'bg-blue-50 dark:bg-blue-500/10 ring-1 ring-blue-400 dark:ring-blue-500 ring-inset'
|
||||
: ''}"
|
||||
role={entry.type === 'directory' ? 'button' : undefined}
|
||||
on:dragover={(e) => {
|
||||
if (entry.type !== 'directory') return;
|
||||
if (!writable) return;
|
||||
@@ -152,11 +171,18 @@
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragOverFolder = true;
|
||||
if (!expanded && !expandTimer) {
|
||||
expandTimer = setTimeout(() => {
|
||||
onToggleExpand(directoryPath);
|
||||
expandTimer = null;
|
||||
}, 600);
|
||||
}
|
||||
}}
|
||||
on:dragleave={(e) => {
|
||||
if (entry.type !== 'directory') return;
|
||||
e.stopPropagation();
|
||||
dragOverFolder = false;
|
||||
clearExpandTimer();
|
||||
}}
|
||||
on:drop={(e) => {
|
||||
if (entry.type !== 'directory') return;
|
||||
@@ -166,10 +192,11 @@
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragOverFolder = false;
|
||||
clearExpandTimer();
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
const paths = data.paths || (data.path ? [data.path] : []);
|
||||
const destFolder = `${currentPath}${entry.name}/`;
|
||||
const destFolder = directoryPath;
|
||||
for (const p of paths) {
|
||||
if (p + '/' === destFolder || p === destFolder) continue;
|
||||
onMove(p, destFolder);
|
||||
@@ -177,15 +204,35 @@
|
||||
} catch {}
|
||||
}}
|
||||
>
|
||||
{#if entry.type === 'directory'}
|
||||
<button
|
||||
type="button"
|
||||
class="mr-1 flex w-4 shrink-0 items-center self-stretch justify-center text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
style="margin-left: {rowIndent};"
|
||||
on:click|stopPropagation={() => onToggleExpand(directoryPath)}
|
||||
aria-label={expanded ? $i18n.t('Collapse') : $i18n.t('Expand')}
|
||||
>
|
||||
<Icon
|
||||
name={expanded ? 'chevron-down' : 'chevron-right'}
|
||||
size={9}
|
||||
strokeWidth={1.5}
|
||||
class={loadingChildren ? 'animate-pulse' : ''}
|
||||
/>
|
||||
</button>
|
||||
{:else}
|
||||
<span class="mr-1 w-4 shrink-0 self-stretch" style="margin-left: {rowIndent};"></span>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="flex-1 flex items-center gap-2 px-3 py-1.5 text-left min-w-0"
|
||||
type="button"
|
||||
class="flex min-w-0 flex-1 items-center gap-2 py-1.5 pr-2 text-left"
|
||||
draggable={canMutate}
|
||||
on:dragstart={(e) => {
|
||||
if (!canMutate) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
const filePath = `${currentPath}${entry.name}`;
|
||||
const filePath = entryPath.replace(/\/$/, '');
|
||||
// If dragging a selected item, drag all selected
|
||||
if (selected && selectedPaths.size > 1) {
|
||||
e.dataTransfer?.setData(
|
||||
@@ -203,7 +250,7 @@
|
||||
} else {
|
||||
e.dataTransfer?.setData(
|
||||
'application/x-terminal-file-move',
|
||||
JSON.stringify({ path: filePath, name: entry.name })
|
||||
JSON.stringify({ path: entry.type === 'directory' ? directoryPath : filePath, name: entry.name })
|
||||
);
|
||||
}
|
||||
if (entry.type === 'file') {
|
||||
@@ -229,34 +276,23 @@
|
||||
{#if selectionMode || selected}
|
||||
<!-- Checkbox indicator -->
|
||||
<div
|
||||
class="size-3.5 shrink-0 rounded border transition-colors flex items-center justify-center
|
||||
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>
|
||||
<Icon name="check" size={10} strokeWidth={2} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<FileTypeIcon name={entry.name} type={entry.type} />
|
||||
<FileTypeIcon name={entry.name} type={entry.type} size={13} />
|
||||
{#if renaming}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<input
|
||||
bind:this={renameInput}
|
||||
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"
|
||||
class="flex-1 text-xs bg-transparent border border-gray-100 dark:border-white/[0.06] 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();
|
||||
@@ -294,65 +330,79 @@
|
||||
|
||||
<Dropdown align="end" sideOffset={4}>
|
||||
<button
|
||||
class="shrink-0 p-0.5 mr-1 rounded-lg transition
|
||||
class="shrink-0 flex h-5 w-5 items-center justify-center mr-1 rounded transition
|
||||
text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400
|
||||
hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
hover:bg-gray-50/40 dark:hover:bg-white/4"
|
||||
aria-label={$i18n.t('More')}
|
||||
>
|
||||
<EllipsisHorizontal className="size-3.5" />
|
||||
<Icon name="three-dots" size={12} strokeWidth={1.4} />
|
||||
</button>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu className="min-w-[9.375rem] z-[9999999]">
|
||||
<button
|
||||
type="button"
|
||||
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition"
|
||||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
const path =
|
||||
entry.type === 'directory'
|
||||
? `${currentPath}${entry.name}/`
|
||||
: `${currentPath}${entry.name}`;
|
||||
onDownload(path);
|
||||
onOpen(entry);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex items-center">{$i18n.t('Download')}</div>
|
||||
<Icon name={entry.type === 'directory' ? 'folder' : 'eye'} size={12} strokeWidth={1.4} />
|
||||
<div class="flex items-center">
|
||||
{entry.type === 'directory' ? $i18n.t('Open Folder') : $i18n.t('Open')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if entry.type === 'directory'}
|
||||
<button
|
||||
type="button"
|
||||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleExpand(directoryPath);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={expanded ? 'chevron-down' : 'chevron-right'}
|
||||
size={12}
|
||||
strokeWidth={1.4}
|
||||
/>
|
||||
<div class="flex items-center">
|
||||
{expanded ? $i18n.t('Collapse') : $i18n.t('Expand')}
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
onDownload(entryPath);
|
||||
}}
|
||||
>
|
||||
<Icon name="download" size={12} strokeWidth={1.4} />
|
||||
<div class="flex items-center">{$i18n.t('Download')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition"
|
||||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
const path =
|
||||
entry.type === 'directory'
|
||||
? `${currentPath}${entry.name}/`
|
||||
: `${currentPath}${entry.name}`;
|
||||
navigator.clipboard.writeText(path).then(() => {
|
||||
navigator.clipboard.writeText(entryPath).then(() => {
|
||||
toast.success($i18n.t('Path copied'));
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Clipboard className="size-3.5" strokeWidth="1.5" />
|
||||
<Icon name="copy" size={12} strokeWidth={1.4} />
|
||||
<div class="flex items-center">{$i18n.t('Copy Path')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
disabled={!canMutate}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -360,21 +410,21 @@
|
||||
startRename();
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-3.5" strokeWidth="1.5" />
|
||||
<Icon name="pencil" size={12} strokeWidth={1.4} />
|
||||
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
disabled={!canMutate}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!canMutate) return;
|
||||
onDelete(`${currentPath}${entry.name}`, entry.name);
|
||||
onDelete(entryPath.replace(/\/$/, ''), entry.name);
|
||||
}}
|
||||
>
|
||||
<GarbageBin className="size-3.5" />
|
||||
<Icon name="trash" size={12} strokeWidth={1.4} />
|
||||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
|
||||
Reference in new issue
Block a user