|
3 | 3 | import FilePane from './FilePane.svelte' |
4 | 4 | import PaneResizer from './PaneResizer.svelte' |
5 | 5 | import LoadingIcon from '../LoadingIcon.svelte' |
| 6 | + import NewFolderDialog from './NewFolderDialog.svelte' |
6 | 7 | import CopyDialog from '../write-operations/CopyDialog.svelte' |
7 | 8 | import CopyProgressDialog from '../write-operations/CopyProgressDialog.svelte' |
8 | 9 | import { toBackendIndices, toBackendCursorIndex } from '../write-operations/copy-dialog-utils' |
|
29 | 30 | updateFocusedPane, |
30 | 31 | getFileAt, |
31 | 32 | getListingStats, |
| 33 | + findFileIndex, |
32 | 34 | } from '$lib/tauri-commands' |
33 | | - import type { VolumeInfo, SortColumn, SortOrder, NetworkHost } from './types' |
| 35 | + import type { VolumeInfo, SortColumn, SortOrder, NetworkHost, DirectoryDiff } from './types' |
34 | 36 | import { defaultSortOrders, DEFAULT_SORT_BY } from './types' |
35 | 37 | import { ensureFontMetricsLoaded } from '$lib/font-metrics' |
| 38 | + import { removeExtension } from './new-folder-utils' |
36 | 39 | import { |
37 | 40 | createHistory, |
38 | 41 | push, |
|
102 | 105 | previewId: string | null |
103 | 106 | } | null>(null) |
104 | 107 |
|
| 108 | + // New folder dialog state |
| 109 | + let showNewFolderDialog = $state(false) |
| 110 | + let newFolderDialogProps = $state<{ |
| 111 | + currentPath: string |
| 112 | + listingId: string |
| 113 | + showHiddenFiles: boolean |
| 114 | + initialName: string |
| 115 | + } | null>(null) |
| 116 | +
|
105 | 117 | // Navigation history for each pane (per-pane, session-only) |
106 | 118 | // Initialize with default volume - will be updated on mount with actual state |
107 | 119 | let leftHistory = $state<NavigationHistory>(createHistory(DEFAULT_VOLUME_ID, '~')) |
|
534 | 546 | return |
535 | 547 | } |
536 | 548 |
|
| 549 | + // F7 - New folder dialog |
| 550 | + if (e.key === 'F7') { |
| 551 | + e.preventDefault() |
| 552 | + void openNewFolderDialog() |
| 553 | + return |
| 554 | + } |
| 555 | +
|
537 | 556 | // Route to volume chooser if one is open |
538 | 557 | if (routeToVolumeChooser(e)) { |
539 | 558 | return |
|
814 | 833 | return paths |
815 | 834 | } |
816 | 835 |
|
| 836 | + /** Gets the initial name for the new folder dialog (dir name as-is, file name without extension). */ |
| 837 | + async function getInitialFolderName(paneRef: FilePane | undefined, paneListingId: string): Promise<string> { |
| 838 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 839 | + const cursorIndex = paneRef?.getCursorIndex?.() as number | undefined |
| 840 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 841 | + const hasParent = paneRef?.hasParentEntry?.() as boolean | undefined |
| 842 | + if (cursorIndex === undefined || cursorIndex < 0) return '' |
| 843 | + const backendIndex = hasParent ? cursorIndex - 1 : cursorIndex |
| 844 | + if (backendIndex < 0) return '' |
| 845 | + const entry = await getFileAt(paneListingId, backendIndex, showHiddenFiles) |
| 846 | + if (!entry) return '' |
| 847 | + return entry.isDirectory ? entry.name : removeExtension(entry.name) |
| 848 | + } |
| 849 | +
|
| 850 | + async function openNewFolderDialog() { |
| 851 | + /** Opens the new folder dialog. Pre-fills with the entry name under cursor. */ |
| 852 | + const isLeft = focusedPane === 'left' |
| 853 | + const paneRef = isLeft ? leftPaneRef : rightPaneRef |
| 854 | + const path = isLeft ? leftPath : rightPath |
| 855 | +
|
| 856 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 857 | + const paneListingId = paneRef?.getListingId?.() as string | undefined |
| 858 | + if (!paneListingId) return |
| 859 | +
|
| 860 | + const initialName = await getInitialFolderName(paneRef, paneListingId) |
| 861 | +
|
| 862 | + newFolderDialogProps = { |
| 863 | + currentPath: path, |
| 864 | + listingId: paneListingId, |
| 865 | + showHiddenFiles, |
| 866 | + initialName, |
| 867 | + } |
| 868 | + showNewFolderDialog = true |
| 869 | + } |
| 870 | +
|
| 871 | + function handleNewFolderCreated(folderName: string) { |
| 872 | + const paneRef = focusedPane === 'left' ? leftPaneRef : rightPaneRef |
| 873 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 874 | + const paneListingId = paneRef?.getListingId?.() as string | undefined |
| 875 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 876 | + const hasParent = paneRef?.hasParentEntry?.() as boolean | undefined |
| 877 | +
|
| 878 | + showNewFolderDialog = false |
| 879 | + newFolderDialogProps = null |
| 880 | + containerElement?.focus() |
| 881 | +
|
| 882 | + // Wait for file watcher to pick up the new folder, then move cursor to it |
| 883 | + if (!paneListingId) return |
| 884 | + void moveCursorToNewFolder(paneListingId, folderName, paneRef, hasParent ?? false) |
| 885 | + } |
| 886 | +
|
| 887 | + /** Waits for the file watcher diff, then moves cursor to the newly created folder. */ |
| 888 | + async function moveCursorToNewFolder( |
| 889 | + paneListingId: string, |
| 890 | + folderName: string, |
| 891 | + paneRef: FilePane | undefined, |
| 892 | + hasParent: boolean, |
| 893 | + ) { |
| 894 | + const unlisten = await listen<DirectoryDiff>('directory-diff', (event) => { |
| 895 | + if (event.payload.listingId !== paneListingId) return |
| 896 | + // Small delay to ensure listing cache is fully updated before querying |
| 897 | + setTimeout(() => { |
| 898 | + void findFileIndex(paneListingId, folderName, showHiddenFiles).then((index) => { |
| 899 | + if (index !== null) { |
| 900 | + const frontendIndex = hasParent ? index + 1 : index |
| 901 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 902 | + paneRef?.setCursorIndex?.(frontendIndex) |
| 903 | + unlisten() |
| 904 | + } |
| 905 | + }) |
| 906 | + }, 50) |
| 907 | + }) |
| 908 | + // Clean up listener after 3 seconds if folder never appears |
| 909 | + setTimeout(() => { |
| 910 | + unlisten() |
| 911 | + }, 3000) |
| 912 | + } |
| 913 | +
|
| 914 | + function handleNewFolderCancel() { |
| 915 | + showNewFolderDialog = false |
| 916 | + newFolderDialogProps = null |
| 917 | + containerElement?.focus() |
| 918 | + } |
| 919 | +
|
817 | 920 | /** Opens the copy dialog with the current selection info. */ |
818 | 921 | async function openCopyDialog() { |
819 | 922 | const isLeft = focusedPane === 'left' |
|
1275 | 1378 | /> |
1276 | 1379 | {/if} |
1277 | 1380 |
|
| 1381 | +{#if showNewFolderDialog && newFolderDialogProps} |
| 1382 | + <NewFolderDialog |
| 1383 | + currentPath={newFolderDialogProps.currentPath} |
| 1384 | + listingId={newFolderDialogProps.listingId} |
| 1385 | + showHiddenFiles={newFolderDialogProps.showHiddenFiles} |
| 1386 | + initialName={newFolderDialogProps.initialName} |
| 1387 | + onCreated={handleNewFolderCreated} |
| 1388 | + onCancel={handleNewFolderCancel} |
| 1389 | + /> |
| 1390 | +{/if} |
| 1391 | + |
1278 | 1392 | <style> |
1279 | 1393 | .dual-pane-explorer { |
1280 | 1394 | display: flex; |
|
0 commit comments