|
1 | 1 | <script lang="ts"> |
2 | | - import { getContext } from 'svelte'; |
| 2 | + import { getContext, tick } from 'svelte'; |
3 | 3 | import { formatFileSize } from '$lib/utils'; |
4 | 4 | import type { FileEntry } from '$lib/apis/terminal'; |
5 | 5 |
|
6 | 6 | import Dropdown from '$lib/components/common/Dropdown.svelte'; |
7 | 7 | import Folder from '../../icons/Folder.svelte'; |
8 | 8 | import EllipsisHorizontal from '../../icons/EllipsisHorizontal.svelte'; |
9 | 9 | import GarbageBin from '../../icons/GarbageBin.svelte'; |
| 10 | + import Pencil from '../../icons/Pencil.svelte'; |
10 | 11 |
|
11 | 12 | const i18n = getContext('i18n'); |
12 | 13 |
|
|
19 | 20 | export let onDownload: (path: string) => void = () => {}; |
20 | 21 | export let onDelete: (path: string, name: string) => void = () => {}; |
21 | 22 | export let onMove: (source: string, destFolder: string) => void = () => {}; |
| 23 | + export let onRename: (oldPath: string, newName: string) => void = () => {}; |
22 | 24 |
|
23 | 25 | let dragOverFolder = false; |
| 26 | +
|
| 27 | + // ── Rename state ───────────────────────────────────────────────────── |
| 28 | + let renaming = false; |
| 29 | + let renameValue = ''; |
| 30 | + let renameInput: HTMLInputElement; |
| 31 | +
|
| 32 | + const startRename = async () => { |
| 33 | + renameValue = entry.name; |
| 34 | + renaming = true; |
| 35 | + await tick(); |
| 36 | + renameInput?.focus(); |
| 37 | + // Select the name without extension for files |
| 38 | + if (entry.type === 'file') { |
| 39 | + const dotIdx = entry.name.lastIndexOf('.'); |
| 40 | + renameInput?.setSelectionRange(0, dotIdx > 0 ? dotIdx : entry.name.length); |
| 41 | + } else { |
| 42 | + renameInput?.select(); |
| 43 | + } |
| 44 | + }; |
| 45 | +
|
| 46 | + const submitRename = () => { |
| 47 | + const newName = renameValue.trim(); |
| 48 | + renaming = false; |
| 49 | + if (!newName || newName === entry.name) return; |
| 50 | + onRename(`${currentPath}${entry.name}`, newName); |
| 51 | + }; |
| 52 | +
|
| 53 | + const cancelRename = () => { |
| 54 | + renaming = false; |
| 55 | + renameValue = ''; |
| 56 | + }; |
24 | 57 | </script> |
25 | 58 |
|
26 | 59 | <li class="group"> |
|
83 | 116 | ); |
84 | 117 | } |
85 | 118 | }} |
86 | | - on:click={() => onOpen(entry)} |
| 119 | + on:click={() => { |
| 120 | + if (!renaming) onOpen(entry); |
| 121 | + }} |
| 122 | + on:dblclick|preventDefault|stopPropagation={() => { |
| 123 | + startRename(); |
| 124 | + }} |
87 | 125 | > |
88 | 126 | {#if entry.type === 'directory'} |
89 | 127 | <Folder className="size-4 shrink-0 text-blue-400 dark:text-blue-300" /> |
|
103 | 141 | /> |
104 | 142 | </svg> |
105 | 143 | {/if} |
106 | | - <span class="flex-1 text-xs text-gray-800 dark:text-gray-200 truncate"> |
107 | | - {entry.name} |
108 | | - </span> |
109 | | - {#if entry.type === 'file' && entry.size !== undefined} |
| 144 | + {#if renaming} |
| 145 | + <!-- svelte-ignore a11y-click-events-have-key-events --> |
| 146 | + <input |
| 147 | + bind:this={renameInput} |
| 148 | + bind:value={renameValue} |
| 149 | + 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" |
| 150 | + on:keydown={(e) => { |
| 151 | + if (e.key === 'Enter') { e.preventDefault(); submitRename(); } |
| 152 | + if (e.key === 'Escape') { e.preventDefault(); cancelRename(); } |
| 153 | + }} |
| 154 | + on:blur={submitRename} |
| 155 | + on:click|stopPropagation |
| 156 | + /> |
| 157 | + {:else} |
| 158 | + <span class="flex-1 text-xs text-gray-800 dark:text-gray-200 truncate"> |
| 159 | + {entry.name} |
| 160 | + </span> |
| 161 | + {/if} |
| 162 | + {#if entry.type === 'file' && entry.size !== undefined && !renaming} |
110 | 163 | <span class="text-xs text-gray-400 shrink-0">{formatFileSize(entry.size)}</span> |
111 | 164 | {/if} |
112 | 165 | </button> |
|
151 | 204 | </button> |
152 | 205 | {/if} |
153 | 206 |
|
| 207 | + <button |
| 208 | + type="button" |
| 209 | + class="select-none flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-sm" |
| 210 | + on:click={(e) => { |
| 211 | + e.stopPropagation(); |
| 212 | + startRename(); |
| 213 | + }} |
| 214 | + > |
| 215 | + <Pencil className="size-4" strokeWidth="1.5" /> |
| 216 | + <div class="flex items-center">{$i18n.t('Rename')}</div> |
| 217 | + </button> |
| 218 | + |
154 | 219 | <button |
155 | 220 | type="button" |
156 | 221 | class="select-none flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-sm" |
|
0 commit comments