|
3 | 3 | import FilePane from './FilePane.svelte' |
4 | 4 | import PaneResizer from './PaneResizer.svelte' |
5 | 5 | import LoadingIcon from '../LoadingIcon.svelte' |
| 6 | + import CopyDialog from '../write-operations/CopyDialog.svelte' |
6 | 7 | import { |
7 | 8 | loadAppStatus, |
8 | 9 | saveAppStatus, |
|
23 | 24 | DEFAULT_VOLUME_ID, |
24 | 25 | type UnlistenFn, |
25 | 26 | updateFocusedPane, |
| 27 | + getFileAt, |
| 28 | + getListingStats, |
26 | 29 | } from '$lib/tauri-commands' |
27 | 30 | import type { VolumeInfo, SortColumn, SortOrder, NetworkHost } from './types' |
28 | 31 | import { defaultSortOrders, DEFAULT_SORT_BY } from './types' |
|
70 | 73 | let unlistenVolumeUnmount: UnlistenFn | undefined |
71 | 74 | let unlistenNavigation: UnlistenFn | undefined |
72 | 75 |
|
| 76 | + // Copy dialog state |
| 77 | + let showCopyDialog = $state(false) |
| 78 | + let copyDialogProps = $state<{ |
| 79 | + sourcePaths: string[] |
| 80 | + destinationPath: string |
| 81 | + direction: 'left' | 'right' |
| 82 | + currentVolumeId: string |
| 83 | + fileCount: number |
| 84 | + folderCount: number |
| 85 | + sourceFolderPath: string |
| 86 | + } | null>(null) |
| 87 | +
|
73 | 88 | // Navigation history for each pane (per-pane, session-only) |
74 | 89 | // Initialize with default volume - will be updated on mount with actual state |
75 | 90 | let leftHistory = $state<NavigationHistory>(createHistory(DEFAULT_VOLUME_ID, '~')) |
|
495 | 510 | return |
496 | 511 | } |
497 | 512 |
|
| 513 | + // F5 - Copy dialog |
| 514 | + if (e.key === 'F5') { |
| 515 | + e.preventDefault() |
| 516 | + void openCopyDialog() |
| 517 | + return |
| 518 | + } |
| 519 | +
|
498 | 520 | // Route to volume chooser if one is open |
499 | 521 | if (routeToVolumeChooser(e)) { |
500 | 522 | return |
|
763 | 785 | void saveAppStatus({ leftPaneWidthPercent: 50 }) |
764 | 786 | } |
765 | 787 |
|
| 788 | + /** Gets file paths for the given indices from a listing. */ |
| 789 | + async function getSelectedFilePaths(listingId: string, indices: number[]): Promise<string[]> { |
| 790 | + const paths: string[] = [] |
| 791 | + for (const index of indices) { |
| 792 | + const file = await getFileAt(listingId, index, showHiddenFiles) |
| 793 | + if (file && file.name !== '..') { |
| 794 | + paths.push(file.path) |
| 795 | + } |
| 796 | + } |
| 797 | + return paths |
| 798 | + } |
| 799 | +
|
| 800 | + /** Opens the copy dialog with the current selection info. */ |
| 801 | + async function openCopyDialog() { |
| 802 | + const isLeft = focusedPane === 'left' |
| 803 | + const sourcePaneRef = isLeft ? leftPaneRef : rightPaneRef |
| 804 | +
|
| 805 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 806 | + const listingId = sourcePaneRef?.getListingId?.() as string | undefined |
| 807 | + if (!listingId) return |
| 808 | +
|
| 809 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 810 | + const selectedIndices = sourcePaneRef?.getSelectedIndices?.() as number[] | undefined |
| 811 | + const hasSelection = selectedIndices && selectedIndices.length > 0 |
| 812 | +
|
| 813 | + const props = hasSelection |
| 814 | + ? await buildCopyPropsFromSelection(listingId, selectedIndices, isLeft) |
| 815 | + : await buildCopyPropsFromCursor(listingId, sourcePaneRef, isLeft) |
| 816 | +
|
| 817 | + if (props) { |
| 818 | + copyDialogProps = props |
| 819 | + showCopyDialog = true |
| 820 | + } |
| 821 | + } |
| 822 | +
|
| 823 | + type CopyDialogPropsData = { |
| 824 | + sourcePaths: string[] |
| 825 | + destinationPath: string |
| 826 | + direction: 'left' | 'right' |
| 827 | + currentVolumeId: string |
| 828 | + fileCount: number |
| 829 | + folderCount: number |
| 830 | + sourceFolderPath: string |
| 831 | + } |
| 832 | +
|
| 833 | + /** Builds copy dialog props from selected files. */ |
| 834 | + async function buildCopyPropsFromSelection( |
| 835 | + listingId: string, |
| 836 | + selectedIndices: number[], |
| 837 | + isLeft: boolean, |
| 838 | + ): Promise<CopyDialogPropsData | null> { |
| 839 | + const stats = await getListingStats(listingId, showHiddenFiles, selectedIndices) |
| 840 | + const sourcePaths = await getSelectedFilePaths(listingId, selectedIndices) |
| 841 | + if (sourcePaths.length === 0) return null |
| 842 | +
|
| 843 | + return { |
| 844 | + sourcePaths, |
| 845 | + destinationPath: isLeft ? rightPath : leftPath, |
| 846 | + direction: isLeft ? 'right' : 'left', |
| 847 | + currentVolumeId: isLeft ? rightVolumeId : leftVolumeId, |
| 848 | + fileCount: stats.selectedFiles ?? 0, |
| 849 | + folderCount: stats.selectedDirs ?? 0, |
| 850 | + sourceFolderPath: isLeft ? leftPath : rightPath, |
| 851 | + } |
| 852 | + } |
| 853 | +
|
| 854 | + /** Builds copy dialog props from the file under cursor. */ |
| 855 | + async function buildCopyPropsFromCursor( |
| 856 | + listingId: string, |
| 857 | + paneRef: FilePane | undefined, |
| 858 | + isLeft: boolean, |
| 859 | + ): Promise<CopyDialogPropsData | null> { |
| 860 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 861 | + const cursorIndex = paneRef?.getCursorIndex?.() as number | undefined |
| 862 | + if (cursorIndex === undefined || cursorIndex < 0) return null |
| 863 | +
|
| 864 | + const file = await getFileAt(listingId, cursorIndex, showHiddenFiles) |
| 865 | + if (!file || file.name === '..') return null |
| 866 | +
|
| 867 | + return { |
| 868 | + sourcePaths: [file.path], |
| 869 | + destinationPath: isLeft ? rightPath : leftPath, |
| 870 | + direction: isLeft ? 'right' : 'left', |
| 871 | + currentVolumeId: isLeft ? rightVolumeId : leftVolumeId, |
| 872 | + fileCount: file.isDirectory ? 0 : 1, |
| 873 | + folderCount: file.isDirectory ? 1 : 0, |
| 874 | + sourceFolderPath: isLeft ? leftPath : rightPath, |
| 875 | + } |
| 876 | + } |
| 877 | +
|
| 878 | + function handleCopyConfirm(destination: string, volumeId: string) { |
| 879 | + // TODO: Implement actual copy operation using copyFiles() from tauri-commands |
| 880 | + const itemCount = copyDialogProps?.sourcePaths.length ?? 0 |
| 881 | + log.info(`Copy confirmed: ${String(itemCount)} items to ${destination} (volume: ${volumeId})`) |
| 882 | + showCopyDialog = false |
| 883 | + copyDialogProps = null |
| 884 | + containerElement?.focus() |
| 885 | + } |
| 886 | +
|
| 887 | + function handleCopyCancel() { |
| 888 | + showCopyDialog = false |
| 889 | + copyDialogProps = null |
| 890 | + containerElement?.focus() |
| 891 | + } |
| 892 | +
|
766 | 893 | // Focus the container after initialization so keyboard events work |
767 | 894 | $effect(() => { |
768 | 895 | if (initialized) { |
|
1038 | 1165 | {/if} |
1039 | 1166 | </div> |
1040 | 1167 |
|
| 1168 | +{#if showCopyDialog && copyDialogProps} |
| 1169 | + <CopyDialog |
| 1170 | + sourcePaths={copyDialogProps.sourcePaths} |
| 1171 | + destinationPath={copyDialogProps.destinationPath} |
| 1172 | + direction={copyDialogProps.direction} |
| 1173 | + {volumes} |
| 1174 | + currentVolumeId={copyDialogProps.currentVolumeId} |
| 1175 | + fileCount={copyDialogProps.fileCount} |
| 1176 | + folderCount={copyDialogProps.folderCount} |
| 1177 | + sourceFolderPath={copyDialogProps.sourceFolderPath} |
| 1178 | + onConfirm={handleCopyConfirm} |
| 1179 | + onCancel={handleCopyCancel} |
| 1180 | + /> |
| 1181 | +{/if} |
| 1182 | + |
1041 | 1183 | <style> |
1042 | 1184 | .dual-pane-explorer { |
1043 | 1185 | display: flex; |
|
0 commit comments