|
11 | 11 | FolderNode,
|
12 | 12 | TreeItemInput,
|
13 | 13 | type FileTreeNode,
|
| 14 | + type PasteOperation, |
| 15 | + type TreeItemState, |
14 | 16 | } from "svelte-file-tree";
|
| 17 | + import { SvelteSet } from "svelte/reactivity"; |
15 | 18 | import files from "./files.json" with { type: "json" };
|
16 | 19 |
|
17 | 20 | type File = {
|
|
49 | 52 | }),
|
50 | 53 | );
|
51 | 54 |
|
| 55 | + const selectedIds = new SvelteSet<string>(); |
| 56 | + const clipboardIds = new SvelteSet<string>(); |
| 57 | + let pasteOperation: PasteOperation | undefined = $state.raw(); |
| 58 | + let focusedItem: TreeItemState<NodeData> | undefined = $state.raw(); |
| 59 | +
|
| 60 | + function getTotalCount(nodes: Array<FileTreeNode<NodeData>>): number { |
| 61 | + let count = 0; |
| 62 | + for (const node of nodes) { |
| 63 | + count++; |
| 64 | +
|
| 65 | + if (node.type === "folder") { |
| 66 | + count += getTotalCount(node.children); |
| 67 | + } |
| 68 | + } |
| 69 | + return count; |
| 70 | + } |
| 71 | +
|
| 72 | + function getTotalSize(nodes: Array<FileTreeNode<NodeData>>): number { |
| 73 | + let size = 0; |
| 74 | + for (const node of nodes) { |
| 75 | + size += node.data.size ?? 0; |
| 76 | +
|
| 77 | + if (node.type === "folder") { |
| 78 | + size += getTotalSize(node.children); |
| 79 | + } |
| 80 | + } |
| 81 | + return size; |
| 82 | + } |
| 83 | +
|
| 84 | + const totalCount = $derived(getTotalCount(tree.children)); |
| 85 | + const totalSize = $derived(getTotalSize(tree.children)); |
| 86 | +
|
| 87 | + const pasteDirection = $derived.by((): string | undefined => { |
| 88 | + if (pasteOperation === undefined || focusedItem === undefined) { |
| 89 | + return; |
| 90 | + } |
| 91 | +
|
| 92 | + if (focusedItem.node.type === "folder" && focusedItem.expanded()) { |
| 93 | + return "Inside"; |
| 94 | + } |
| 95 | +
|
| 96 | + return "After"; |
| 97 | + }); |
| 98 | +
|
52 | 99 | function getFileKind(name: string): string | undefined {
|
53 | 100 | const dotIndex = name.lastIndexOf(".");
|
54 | 101 | if (dotIndex === -1) {
|
|
267 | 314 | return "-";
|
268 | 315 | }
|
269 | 316 |
|
270 |
| - if (size < 1_000) { |
| 317 | + if (size < 1000) { |
271 | 318 | return sizeFormatter.format(size) + " B";
|
272 | 319 | }
|
273 | 320 |
|
274 |
| - size /= 1_000; |
275 |
| - if (size < 1_000) { |
| 321 | + size /= 1000; |
| 322 | + if (size < 1000) { |
276 | 323 | return sizeFormatter.format(size) + " KB";
|
277 | 324 | }
|
278 | 325 |
|
279 |
| - size /= 1_000; |
280 |
| - if (size < 1_000) { |
| 326 | + size /= 1000; |
| 327 | + if (size < 1000) { |
281 | 328 | return sizeFormatter.format(size) + " MB";
|
282 | 329 | }
|
283 | 330 |
|
284 | 331 | size /= 1000;
|
285 |
| - if (size < 1_000) { |
| 332 | + if (size < 1000) { |
286 | 333 | return sizeFormatter.format(size) + " GB";
|
287 | 334 | }
|
288 | 335 |
|
289 | 336 | size /= 1000;
|
290 | 337 | return sizeFormatter.format(size) + " TB";
|
291 | 338 | }
|
292 | 339 |
|
293 |
| - function onUploadFiles({ |
| 340 | + function handleUploadFiles({ |
294 | 341 | target,
|
295 | 342 | files,
|
296 | 343 | }: {
|
|
310 | 357 | }
|
311 | 358 | }
|
312 | 359 |
|
313 |
| - function onCreateNewFolder({ |
| 360 | + function handleCreateNewFolder({ |
314 | 361 | target,
|
315 | 362 | name,
|
316 | 363 | }: {
|
|
326 | 373 | }
|
327 | 374 | </script>
|
328 | 375 |
|
329 |
| -<main class="flex min-h-svh flex-col p-8"> |
330 |
| - <TreeContextMenu {tree} {onUploadFiles} {onCreateNewFolder}> |
331 |
| - <TreeContextMenuTrigger class="grow rounded border border-gray-400 p-5"> |
332 |
| - <div |
333 |
| - class="grid grid-cols-(--grid-cols) gap-x-(--grid-gap) px-(--grid-inline-padding) text-sm font-semibold" |
334 |
| - > |
335 |
| - <div>Name</div> |
336 |
| - <div>Size</div> |
337 |
| - <div>Kind</div> |
338 |
| - </div> |
| 376 | +<main class="flex h-svh flex-col"> |
| 377 | + <div |
| 378 | + class="grid grid-cols-(--grid-cols) gap-x-(--grid-gap) border-b border-gray-300 px-[calc(var(--tree-inline-padding)+var(--grid-inline-padding))] py-3 text-sm font-semibold" |
| 379 | + > |
| 380 | + <div>Name</div> |
| 381 | + <div>Size</div> |
| 382 | + <div>Kind</div> |
| 383 | + </div> |
339 | 384 |
|
340 |
| - <Tree {tree} isItemEditable class="mt-2"> |
| 385 | + <TreeContextMenu |
| 386 | + {tree} |
| 387 | + onUploadFiles={handleUploadFiles} |
| 388 | + onCreateNewFolder={handleCreateNewFolder} |
| 389 | + > |
| 390 | + <TreeContextMenuTrigger class="grow overflow-y-auto rounded px-(--tree-inline-padding) py-2"> |
| 391 | + <Tree |
| 392 | + {tree} |
| 393 | + {selectedIds} |
| 394 | + {clipboardIds} |
| 395 | + bind:pasteOperation |
| 396 | + isItemEditable |
| 397 | + onfocusout={() => { |
| 398 | + focusedItem = undefined; |
| 399 | + }} |
| 400 | + > |
341 | 401 | {#snippet item({ item, expand, collapse, copy, paste, remove })}
|
342 | 402 | <TreeItem
|
343 | 403 | {item}
|
344 | 404 | draggable
|
345 |
| - onCopy={copy} |
346 |
| - onPaste={paste} |
347 |
| - onDelete={remove} |
348 | 405 | class={({ dropPosition }) => [
|
349 | 406 | "relative grid grid-cols-(--grid-cols) gap-x-(--grid-gap) rounded-md p-(--grid-inline-padding) hover:bg-neutral-200 focus:outline-2 focus:-outline-offset-2 focus:outline-current active:bg-neutral-300 aria-selected:bg-blue-200 aria-selected:text-blue-900 aria-selected:active:bg-blue-300 aria-selected:has-[+[aria-selected='true']]:rounded-b-none aria-selected:[&+[aria-selected='true']]:rounded-t-none",
|
350 | 407 | item.dragged() && "opacity-50",
|
|
354 | 411 | dropPosition() === "after" && "before:border-neutral-300 before:border-b-red-500",
|
355 | 412 | dropPosition() === "inside" && "before:border-red-500",
|
356 | 413 | ]}
|
| 414 | + onCopy={copy} |
| 415 | + onPaste={paste} |
| 416 | + onDelete={remove} |
| 417 | + onfocusin={() => { |
| 418 | + focusedItem = item; |
| 419 | + }} |
357 | 420 | >
|
358 | 421 | {#snippet children({ editing })}
|
359 |
| - <div class="flex items-center" style="padding-inline-start: {item.depth * 24}px"> |
| 422 | + <div |
| 423 | + class="flex items-center" |
| 424 | + style="padding-inline-start: calc(var(--spacing) * {item.depth * 6})" |
| 425 | + > |
360 | 426 | <TreeItemToggle {item} onExpand={expand} onCollapse={collapse} />
|
361 | 427 |
|
362 |
| - <div class="ms-1 me-2"> |
| 428 | + <div class="ps-1 pe-2"> |
363 | 429 | {#if item.node.type === "file"}
|
364 | 430 | <FileIcon role="presentation" />
|
365 | 431 | {:else if item.expanded()}
|
|
383 | 449 | </Tree>
|
384 | 450 | </TreeContextMenuTrigger>
|
385 | 451 | </TreeContextMenu>
|
| 452 | + |
| 453 | + <div class="grid shrink-0 grid-cols-5 place-items-center bg-gray-200 p-2 text-sm"> |
| 454 | + <div> |
| 455 | + <span class="font-medium text-gray-700">Items:</span> |
| 456 | + <span class="font-semibold text-gray-900">{totalCount}</span> |
| 457 | + </div> |
| 458 | + |
| 459 | + <div> |
| 460 | + <span class="font-medium text-gray-700">Selected:</span> |
| 461 | + <span class="font-semibold text-gray-900">{selectedIds.size}</span> |
| 462 | + </div> |
| 463 | + |
| 464 | + <div> |
| 465 | + <span class="font-medium text-gray-700">Clipboard:</span> |
| 466 | + <span class="font-semibold text-gray-900">{clipboardIds.size}</span> |
| 467 | + </div> |
| 468 | + |
| 469 | + <div> |
| 470 | + <span class="font-medium text-gray-700">Paste:</span> |
| 471 | + <span class="font-semibold text-gray-900">{pasteDirection}</span> |
| 472 | + </div> |
| 473 | + |
| 474 | + <div> |
| 475 | + <span class="font-medium text-gray-700">Total Size:</span> |
| 476 | + <span class="font-semibold text-gray-900">{formatSize(totalSize)}</span> |
| 477 | + </div> |
| 478 | + </div> |
386 | 479 | </main>
|
387 | 480 |
|
388 | 481 | <style>
|
389 | 482 | :root {
|
390 | 483 | --grid-cols: 5fr 1fr 2fr;
|
391 | 484 | --grid-gap: calc(var(--spacing) * 4);
|
392 | 485 | --grid-inline-padding: calc(var(--spacing) * 3);
|
| 486 | + --tree-inline-padding: calc(var(--spacing) * 6); |
393 | 487 | }
|
394 | 488 | </style>
|
0 commit comments