refac
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import GarbageBin from '../../icons/GarbageBin.svelte';
|
||||
import Tooltip from '../../common/Tooltip.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let count: number = 0;
|
||||
export let hasFiles: boolean = false;
|
||||
export let canDelete = true;
|
||||
|
||||
export let onDelete: () => void = () => {};
|
||||
@@ -15,81 +14,51 @@
|
||||
export let onClear: () => void = () => {};
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 bg-gray-50 dark:bg-gray-800/50 shrink-0">
|
||||
<span class="text-xs font-normal text-gray-600 dark:text-gray-400 flex-1 truncate">
|
||||
<div
|
||||
class="flex h-8 items-center gap-2 px-2 bg-blue-50 dark:bg-blue-500/5 shrink-0 border-t border-gray-50 dark:border-gray-850/30"
|
||||
>
|
||||
<span class="text-[0.6875rem] font-medium text-blue-600 dark:text-blue-400 flex-1 truncate">
|
||||
{$i18n.t('{{count}} selected', { count })}
|
||||
</span>
|
||||
|
||||
<Tooltip content={$i18n.t('Select All')}>
|
||||
<button
|
||||
class="p-1 rounded transition text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
class="flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click={onSelectAll}
|
||||
aria-label={$i18n.t('Select All')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3.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={12} strokeWidth={1.6} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('Download')}>
|
||||
<button
|
||||
class="p-1 rounded transition text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
class="flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click={onDownload}
|
||||
aria-label={$i18n.t('Download')}
|
||||
>
|
||||
<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>
|
||||
<Icon name="download" size={12} strokeWidth={1.4} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="p-1 rounded transition text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
|
||||
class="flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-red-500 disabled:opacity-30 disabled:hover:text-gray-400"
|
||||
on:click={onDelete}
|
||||
disabled={!canDelete}
|
||||
aria-label={$i18n.t('Delete')}
|
||||
>
|
||||
<GarbageBin className="size-3.5" />
|
||||
<Icon name="trash" size={12} strokeWidth={1.4} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('Deselect')}>
|
||||
<button
|
||||
class="p-1 rounded transition text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
class="flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click={onClear}
|
||||
aria-label={$i18n.t('Deselect')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="xmark" size={12} strokeWidth={1.5} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
export let onUploadFiles: (files: File[]) => void = () => {};
|
||||
export let onDownloadDir: () => void = () => {};
|
||||
export let onMove: (source: string, destFolder: string) => void = () => {};
|
||||
export let showHidden = false;
|
||||
export let onToggleHidden: () => void = () => {};
|
||||
|
||||
// Sort controls
|
||||
export let sortBy: 'name' | 'date' = 'name';
|
||||
export let sortBy: 'name' | 'size' | 'date' = 'name';
|
||||
export let sortAsc: boolean = true;
|
||||
export let onSort: (mode: 'name' | 'date') => void = () => {};
|
||||
export let onSort: (mode: 'name' | 'size' | 'date') => void = () => {};
|
||||
|
||||
// Back / forward navigation
|
||||
export let canGoBack = false;
|
||||
@@ -46,45 +48,45 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex items-center px-2 pb-1.5 shrink-0 gap-1">
|
||||
<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">
|
||||
<!-- Back -->
|
||||
<Tooltip content={$i18n.t('Back')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded transition {canGoBack
|
||||
? 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400'
|
||||
class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 {canGoBack
|
||||
? 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'
|
||||
: 'text-gray-200 dark:text-gray-700 cursor-default'}"
|
||||
on:click={onGoBack}
|
||||
disabled={!canGoBack}
|
||||
aria-label={$i18n.t('Back')}
|
||||
>
|
||||
<Icon name="chevron-left" size={14} strokeWidth={1.5} />
|
||||
<Icon name="chevron-left" size={11} strokeWidth={1.5} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<!-- Forward -->
|
||||
<Tooltip content={$i18n.t('Forward')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded transition {canGoForward
|
||||
? 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400'
|
||||
class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 {canGoForward
|
||||
? 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'
|
||||
: 'text-gray-200 dark:text-gray-700 cursor-default'}"
|
||||
on:click={onGoForward}
|
||||
disabled={!canGoForward}
|
||||
aria-label={$i18n.t('Forward')}
|
||||
>
|
||||
<Icon name="chevron-right" size={14} strokeWidth={1.5} />
|
||||
<Icon name="chevron-right" size={11} strokeWidth={1.5} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
bind:this={breadcrumbEl}
|
||||
class="flex items-center flex-1 min-w-0 overflow-x-auto scrollbar-none"
|
||||
class="flex items-center gap-1 flex-1 min-w-0 overflow-x-auto scrollbar-none"
|
||||
>
|
||||
{#each breadcrumbs as crumb, i}
|
||||
{#if showSeparator(i)}
|
||||
<span class="text-gray-300 dark:text-gray-600 text-xs shrink-0 select-none mx-0.5">/</span>
|
||||
<span class="text-gray-300 dark:text-gray-600 text-xs shrink-0 select-none">/</span>
|
||||
{/if}
|
||||
<button
|
||||
class="text-xs shrink-0 px-1 py-0.5 rounded transition
|
||||
class="text-xs shrink-0 p-0 transition
|
||||
{!selectedFile && i === breadcrumbs.length - 1
|
||||
? 'text-gray-700 dark:text-gray-300'
|
||||
: 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400'}
|
||||
@@ -120,8 +122,8 @@
|
||||
</button>
|
||||
{/each}
|
||||
{#if selectedFile}
|
||||
<span class="text-gray-300 dark:text-gray-600 text-xs shrink-0 select-none mx-0.5">/</span>
|
||||
<span class="text-xs shrink-0 px-1.5 py-0.5 text-gray-700 dark:text-gray-300">
|
||||
<span class="text-gray-300 dark:text-gray-600 text-xs shrink-0 select-none">/</span>
|
||||
<span class="text-xs shrink-0 p-0 text-gray-700 dark:text-gray-300">
|
||||
{selectedFile.split('/').pop()}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -132,11 +134,11 @@
|
||||
|
||||
<Tooltip content={$i18n.t('Refresh')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click={onRefresh}
|
||||
aria-label={$i18n.t('Refresh')}
|
||||
>
|
||||
<Icon name="refresh" size={14} strokeWidth={1.4} class={loading ? 'animate-spin' : ''} />
|
||||
<Icon name="refresh" size={11} strokeWidth={1.4} class={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -144,10 +146,10 @@
|
||||
<Dropdown align="end" sideOffset={4}>
|
||||
<Tooltip content={$i18n.t('Sort')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Sort')}
|
||||
>
|
||||
<Icon name="sort" size={14} strokeWidth={1.4} />
|
||||
<Icon name="sort" size={11} strokeWidth={1.4} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -155,7 +157,7 @@
|
||||
<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={() => onSort('name')}
|
||||
>
|
||||
<span class="flex-1 text-left">{$i18n.t('Name')}</span>
|
||||
@@ -172,7 +174,24 @@
|
||||
</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"
|
||||
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={() => onSort('size')}
|
||||
>
|
||||
<span class="flex-1 text-left">{$i18n.t('Size')}</span>
|
||||
{#if sortBy === 'size'}
|
||||
<Icon
|
||||
name="chevron-up"
|
||||
size={12}
|
||||
strokeWidth={1.5}
|
||||
class="text-gray-500 dark:text-gray-400 transition-transform {sortAsc
|
||||
? ''
|
||||
: 'rotate-180'}"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
<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={() => onSort('date')}
|
||||
>
|
||||
<span class="flex-1 text-left">{$i18n.t('Date Modified')}</span>
|
||||
@@ -190,45 +209,64 @@
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Tooltip content={$i18n.t('New Folder')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
|
||||
on:click={onNewFolder}
|
||||
disabled={!writable}
|
||||
aria-label={$i18n.t('New Folder')}
|
||||
>
|
||||
<Icon name="folder" size={14} strokeWidth={1.4} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={$i18n.t('New File')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
|
||||
on:click={onNewFile}
|
||||
disabled={!writable}
|
||||
aria-label={$i18n.t('New File')}
|
||||
>
|
||||
<Icon name="empty-page" size={14} strokeWidth={1.4} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={$i18n.t('Download')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
on:click={onDownloadDir}
|
||||
aria-label={$i18n.t('Download')}
|
||||
>
|
||||
<Icon name="download" size={14} strokeWidth={1.4} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={$i18n.t('Upload')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
|
||||
on:click={() => uploadInput?.click()}
|
||||
disabled={!writable}
|
||||
aria-label={$i18n.t('Upload')}
|
||||
>
|
||||
<Icon name="upload" size={14} strokeWidth={1.4} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Dropdown align="end" sideOffset={4}>
|
||||
<Tooltip content={$i18n.t('Actions')}>
|
||||
<button
|
||||
class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Actions')}
|
||||
>
|
||||
<Icon name="three-dots" size={11} strokeWidth={1.4} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu className="min-w-[9.375rem] z-[9999999]">
|
||||
<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 disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
on:click={onNewFolder}
|
||||
disabled={!writable}
|
||||
>
|
||||
<Icon name="folder" size={12} strokeWidth={1.4} />
|
||||
<span>{$i18n.t('New Folder')}</span>
|
||||
</button>
|
||||
<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 disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
on:click={onNewFile}
|
||||
disabled={!writable}
|
||||
>
|
||||
<Icon name="empty-page" size={12} strokeWidth={1.4} />
|
||||
<span>{$i18n.t('New File')}</span>
|
||||
</button>
|
||||
<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 disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
on:click={() => uploadInput?.click()}
|
||||
disabled={!writable}
|
||||
>
|
||||
<Icon name="upload" size={12} strokeWidth={1.4} />
|
||||
<span>{$i18n.t('Upload')}</span>
|
||||
</button>
|
||||
<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={onDownloadDir}
|
||||
>
|
||||
<Icon name="download" size={12} strokeWidth={1.4} />
|
||||
<span>{$i18n.t('Download')}</span>
|
||||
</button>
|
||||
<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={onToggleHidden}
|
||||
>
|
||||
<Icon name="eye" size={12} strokeWidth={1.4} />
|
||||
<span>{showHidden ? $i18n.t('Hide Hidden Files') : $i18n.t('Show Hidden Files')}</span>
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<input
|
||||
bind:this={uploadInput}
|
||||
type="file"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { ListeningPort } from '$lib/apis/terminal';
|
||||
import { getListeningPorts, getPortProxyUrl } from '$lib/apis/terminal';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher<{ previewPort: number }>();
|
||||
@@ -55,21 +56,15 @@
|
||||
|
||||
<div class="px-2 py-1">
|
||||
<button
|
||||
class="flex items-center gap-1 w-full text-xs font-normal text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
class="flex items-center gap-1 w-full text-xs font-normal text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-100"
|
||||
on:click={() => (expanded = !expanded)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3 transition-transform {expanded ? '' : '-rotate-90'}"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
size={12}
|
||||
strokeWidth={1.4}
|
||||
class="transition-transform {expanded ? '' : '-rotate-90'}"
|
||||
/>
|
||||
{$i18n.t('Ports')}
|
||||
<span class="ml-auto flex items-center gap-1">
|
||||
{#if ports.length > 0}
|
||||
@@ -81,22 +76,11 @@
|
||||
{/if}
|
||||
<Tooltip content={$i18n.t('Refresh')}>
|
||||
<button
|
||||
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
class="flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click|stopPropagation={loadPorts}
|
||||
aria-label={$i18n.t('Refresh')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3 {loading ? 'animate-spin' : ''}"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="refresh" size={11} strokeWidth={1.4} class={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
@@ -111,7 +95,7 @@
|
||||
{:else}
|
||||
{#each ports as port}
|
||||
<button
|
||||
class="flex items-center w-full gap-2 px-1.5 py-1 text-xs rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition group"
|
||||
class="flex h-7 items-center w-full gap-2 px-1.5 text-xs rounded-lg hover:bg-gray-50/40 dark:hover:bg-white/4 transition-colors duration-75 group"
|
||||
on:click={() => previewPort(port.port)}
|
||||
>
|
||||
<span class="font-mono text-blue-500 dark:text-blue-400 shrink-0">
|
||||
@@ -125,21 +109,10 @@
|
||||
<span
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
class="text-gray-400 dark:text-gray-500 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition shrink-0 p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
class="text-gray-400 dark:text-gray-500 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition shrink-0 flex h-5 w-5 items-center justify-center rounded hover:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click|stopPropagation={() => openPortExternal(port.port)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.25 5.5a.75.75 0 0 0-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75v-4a.75.75 0 0 1 1.5 0v4A2.25 2.25 0 0 1 12.75 17h-8.5A2.25 2.25 0 0 1 2 14.75v-8.5A2.25 2.25 0 0 1 4.25 4h5a.75.75 0 0 1 0 1.5h-5Zm7.5-3.5a.75.75 0 0 0 0 1.5h2.69l-4.72 4.72a.75.75 0 0 0 1.06 1.06l4.72-4.72v2.69a.75.75 0 0 0 1.5 0v-5.25a.75.75 0 0 0-.75-.75h-5.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="external-link" size={11} strokeWidth={1.4} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user