|
149 | 149 | let leftPaneWidthPercent = $state(50) |
150 | 150 |
|
151 | 151 | let containerElement: HTMLDivElement | undefined = $state() |
152 | | - let leftPaneRef: FilePane | undefined = $state() |
153 | | - let rightPaneRef: FilePane | undefined = $state() |
| 152 | + const paneRefs = $state<Record<'left' | 'right', FilePane | undefined>>({ left: undefined, right: undefined }) |
154 | 153 | let unlistenSettings: UnlistenFn | undefined |
155 | 154 | let unlistenViewMode: UnlistenFn | undefined |
156 | 155 | let unlistenVolumeMount: UnlistenFn | undefined |
|
216 | 215 | let dropTargetFolderEl = $state<HTMLElement | null>(null) |
217 | 216 |
|
218 | 217 | // Refs for pane wrapper elements (used for hit-testing drop targets) |
219 | | - let leftPaneWrapperEl: HTMLDivElement | undefined = $state() |
220 | | - let rightPaneWrapperEl: HTMLDivElement | undefined = $state() |
| 218 | + const paneWrapperEls = $state<Record<'left' | 'right', HTMLDivElement | undefined>>({ |
| 219 | + left: undefined, |
| 220 | + right: undefined, |
| 221 | + }) |
221 | 222 |
|
222 | 223 | // Dialog state (transfer, new folder, alert, error) |
223 | 224 | const dialogs = createDialogState({ |
224 | | - getLeftPaneRef: () => leftPaneRef, |
225 | | - getRightPaneRef: () => rightPaneRef, |
| 225 | + getLeftPaneRef: () => paneRefs.left, |
| 226 | + getRightPaneRef: () => paneRefs.right, |
226 | 227 | getFocusedPaneRef: () => getPaneRef(focusedPane), |
227 | 228 | getShowHiddenFiles: () => showHiddenFiles, |
228 | 229 | onRefocus: () => containerElement?.focus(), |
|
236 | 237 | // --- Pane accessor helpers --- |
237 | 238 |
|
238 | 239 | function getPaneRef(pane: 'left' | 'right'): FilePane | undefined { |
239 | | - return pane === 'left' ? leftPaneRef : rightPaneRef |
| 240 | + return paneRefs[pane] |
240 | 241 | } |
241 | 242 |
|
242 | 243 | function getPanePath(pane: 'left' | 'right'): string { |
|
283 | 284 | getActiveTab(getTabMgr(pane)).viewMode = viewMode |
284 | 285 | } |
285 | 286 |
|
| 287 | + function getPaneViewMode(pane: 'left' | 'right'): ViewMode { |
| 288 | + return pane === 'left' ? leftViewMode : rightViewMode |
| 289 | + } |
| 290 | +
|
| 291 | + function getPaneVolumePath(pane: 'left' | 'right'): string { |
| 292 | + return pane === 'left' ? leftVolumePath : rightVolumePath |
| 293 | + } |
| 294 | +
|
| 295 | + function getPaneVolumeName(pane: 'left' | 'right'): string | undefined { |
| 296 | + return pane === 'left' ? leftVolumeName : rightVolumeName |
| 297 | + } |
| 298 | +
|
| 299 | + function getPaneWidth(pane: 'left' | 'right'): number { |
| 300 | + return pane === 'left' ? leftPaneWidthPercent : 100 - leftPaneWidthPercent |
| 301 | + } |
| 302 | +
|
286 | 303 | function otherPane(pane: 'left' | 'right'): 'left' | 'right' { |
287 | 304 | return pane === 'left' ? 'right' : 'left' |
288 | 305 | } |
|
570 | 587 | function routeToVolumeChooser(e: KeyboardEvent): boolean { |
571 | 588 | // Check if EITHER pane has a volume chooser open - if so, route events there |
572 | 589 | // This is important because F1/F2 can open a volume chooser on the non-focused pane |
573 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
574 | | - if (leftPaneRef?.isVolumeChooserOpen?.()) { |
| 590 | + for (const side of ['left', 'right'] as const) { |
| 591 | + const ref = getPaneRef(side) |
575 | 592 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
576 | | - if (leftPaneRef.handleVolumeChooserKeyDown?.(e)) { |
577 | | - return true |
578 | | - } |
579 | | - } |
580 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
581 | | - if (rightPaneRef?.isVolumeChooserOpen?.()) { |
582 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
583 | | - if (rightPaneRef.handleVolumeChooserKeyDown?.(e)) { |
584 | | - return true |
| 593 | + if (ref?.isVolumeChooserOpen?.()) { |
| 594 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 595 | + if (ref.handleVolumeChooserKeyDown?.(e)) { |
| 596 | + return true |
| 597 | + } |
585 | 598 | } |
586 | 599 | } |
587 | 600 | return false |
|
609 | 622 | switch (e.key) { |
610 | 623 | case 'F1': |
611 | 624 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
612 | | - rightPaneRef?.closeVolumeChooser() |
| 625 | + getPaneRef('right')?.closeVolumeChooser() |
613 | 626 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
614 | | - leftPaneRef?.toggleVolumeChooser() |
| 627 | + getPaneRef('left')?.toggleVolumeChooser() |
615 | 628 | return true |
616 | 629 | case 'F2': |
617 | 630 | startRename() |
|
759 | 772 |
|
760 | 773 | /** Updates drop-target highlights and overlay as the cursor moves during a drag. */ |
761 | 774 | function handleDragOver(position: { x: number; y: number }) { |
762 | | - const resolved = resolveDropTarget(position.x, position.y, leftPaneWrapperEl, rightPaneWrapperEl) |
| 775 | + const resolved = resolveDropTarget(position.x, position.y, paneWrapperEls.left, paneWrapperEls.right) |
763 | 776 |
|
764 | 777 | if (resolved?.type === 'folder') { |
765 | 778 | dropTargetPane = null |
|
786 | 799 |
|
787 | 800 | /** Handles the drop event: resolves the target and opens the transfer dialog. */ |
788 | 801 | function handleDrop(paths: string[], position: { x: number; y: number }) { |
789 | | - const resolved = resolveDropTarget(position.x, position.y, leftPaneWrapperEl, rightPaneWrapperEl) |
| 802 | + const resolved = resolveDropTarget(position.x, position.y, paneWrapperEls.left, paneWrapperEls.right) |
790 | 803 | const folderPath = dropTargetFolderPath |
791 | 804 |
|
792 | 805 | // Read the modifier BEFORE stopping the tracker (which resets altKeyHeld) |
|
836 | 849 | shouldRefresh: boolean, |
837 | 850 | throttleUntil: number, |
838 | 851 | setThrottle: (v: number) => void, |
839 | | - paneRef: typeof leftPaneRef, |
| 852 | + paneRef: FilePane | undefined, |
840 | 853 | ) { |
841 | 854 | if (!shouldRefresh) return |
842 | 855 | const now = Date.now() |
|
851 | 864 | const refreshLeft = hasDescendantUpdate(paths, ensureTrailingSlash(leftPath)) |
852 | 865 | const refreshRight = hasDescendantUpdate(paths, ensureTrailingSlash(rightPath)) |
853 | 866 |
|
854 | | - throttledRefresh(refreshLeft, leftThrottleUntil, (v) => (leftThrottleUntil = v), leftPaneRef) |
855 | | - throttledRefresh(refreshRight, rightThrottleUntil, (v) => (rightThrottleUntil = v), rightPaneRef) |
| 867 | + throttledRefresh(refreshLeft, leftThrottleUntil, (v) => (leftThrottleUntil = v), getPaneRef('left')) |
| 868 | + throttledRefresh(refreshRight, rightThrottleUntil, (v) => (rightThrottleUntil = v), getPaneRef('right')) |
856 | 869 | } |
857 | 870 |
|
858 | 871 | function handleResizeForDevTools() { |
|
1208 | 1221 |
|
1209 | 1222 | /** Cancels any active inline rename on either pane. */ |
1210 | 1223 | export function cancelRename() { |
1211 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
1212 | | - leftPaneRef?.cancelRename?.() |
1213 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
1214 | | - rightPaneRef?.cancelRename?.() |
| 1224 | + for (const side of ['left', 'right'] as const) { |
| 1225 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 1226 | + getPaneRef(side)?.cancelRename?.() |
| 1227 | + } |
1215 | 1228 | } |
1216 | 1229 |
|
1217 | 1230 | /** Returns whether inline rename is active on either pane. */ |
1218 | 1231 | export function isRenaming(): boolean { |
1219 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
1220 | | - return (leftPaneRef?.isRenaming?.() as boolean) || (rightPaneRef?.isRenaming?.() as boolean) || false |
| 1232 | + return (['left', 'right'] as const).some((side) => { |
| 1233 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 1234 | + return getPaneRef(side)?.isRenaming?.() as boolean |
| 1235 | + }) |
1221 | 1236 | } |
1222 | 1237 |
|
1223 | 1238 | /** Opens the new folder dialog. Pre-fills with the entry name under cursor. */ |
|
1496 | 1511 | * Close volume chooser on all panes. |
1497 | 1512 | */ |
1498 | 1513 | export function closeVolumeChooser() { |
1499 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
1500 | | - leftPaneRef?.closeVolumeChooser() |
1501 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
1502 | | - rightPaneRef?.closeVolumeChooser() |
| 1514 | + for (const side of ['left', 'right'] as const) { |
| 1515 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 1516 | + getPaneRef(side)?.closeVolumeChooser() |
| 1517 | + } |
1503 | 1518 | } |
1504 | 1519 |
|
1505 | 1520 | /** |
|
1897 | 1912 | } |
1898 | 1913 | </script> |
1899 | 1914 |
|
| 1915 | +{#snippet paneBlock(paneId: 'left' | 'right')} |
| 1916 | + {@const tabMgr = getTabMgr(paneId)} |
| 1917 | + <div |
| 1918 | + class="pane-wrapper" |
| 1919 | + class:drop-target-active={dropTargetPane === paneId} |
| 1920 | + style="width: {getPaneWidth(paneId)}%" |
| 1921 | + bind:this={paneWrapperEls[paneId]} |
| 1922 | + > |
| 1923 | + <TabBar |
| 1924 | + tabs={getAllTabs(tabMgr)} |
| 1925 | + activeTabId={tabMgr.activeTabId} |
| 1926 | + {paneId} |
| 1927 | + maxTabs={MAX_TABS_PER_PANE} |
| 1928 | + onTabSwitch={(tabId: TabId) => { |
| 1929 | + switchToTab(paneId, tabId) |
| 1930 | + }} |
| 1931 | + onTabClose={(tabId: TabId) => { |
| 1932 | + handleTabClose(paneId, tabId) |
| 1933 | + }} |
| 1934 | + onTabMiddleClick={(tabId: TabId) => { |
| 1935 | + handleTabMiddleClick(paneId, tabId) |
| 1936 | + }} |
| 1937 | + onNewTab={() => { |
| 1938 | + handleNewTab(paneId) |
| 1939 | + }} |
| 1940 | + onContextMenu={(tabId: TabId, event: MouseEvent) => { |
| 1941 | + handleTabContextMenu(paneId, tabId, event) |
| 1942 | + }} |
| 1943 | + onPaneFocus={() => { |
| 1944 | + handleFocus(paneId) |
| 1945 | + }} |
| 1946 | + /> |
| 1947 | + <!--suppress JSUnresolvedReference --> |
| 1948 | + {#key getActiveTab(tabMgr).id} |
| 1949 | + <FilePane |
| 1950 | + bind:this={paneRefs[paneId]} |
| 1951 | + {paneId} |
| 1952 | + initialPath={getPanePath(paneId)} |
| 1953 | + volumeId={getPaneVolumeId(paneId)} |
| 1954 | + volumePath={getPaneVolumePath(paneId)} |
| 1955 | + volumeName={getPaneVolumeName(paneId)} |
| 1956 | + isFocused={focusedPane === paneId} |
| 1957 | + {showHiddenFiles} |
| 1958 | + viewMode={getPaneViewMode(paneId)} |
| 1959 | + sortBy={getPaneSort(paneId).sortBy} |
| 1960 | + sortOrder={getPaneSort(paneId).sortOrder} |
| 1961 | + directorySortMode={getDirectorySortMode()} |
| 1962 | + onPathChange={(path: string) => { |
| 1963 | + handlePathChange(paneId, path) |
| 1964 | + }} |
| 1965 | + onVolumeChange={(volumeId: string, volumePath: string, targetPath: string) => |
| 1966 | + handleVolumeChange(paneId, volumeId, volumePath, targetPath)} |
| 1967 | + onRequestFocus={() => { |
| 1968 | + handleFocus(paneId) |
| 1969 | + }} |
| 1970 | + onSortChange={(column: SortColumn) => handleSortChange(paneId, column)} |
| 1971 | + onNetworkHostChange={(host: NetworkHost | null) => { |
| 1972 | + handleNetworkHostChange(paneId, host) |
| 1973 | + }} |
| 1974 | + onCancelLoading={() => { |
| 1975 | + handleCancelLoading(paneId) |
| 1976 | + }} |
| 1977 | + onMtpFatalError={(msg: string) => handleMtpFatalError(paneId, msg)} |
| 1978 | + /> |
| 1979 | + {/key} |
| 1980 | + </div> |
| 1981 | +{/snippet} |
| 1982 | + |
1900 | 1983 | <!-- svelte-ignore a11y_no_noninteractive_tabindex,a11y_no_noninteractive_element_interactions --> |
1901 | 1984 | <div |
1902 | 1985 | class="dual-pane-explorer" |
|
1908 | 1991 | aria-label="File explorer" |
1909 | 1992 | > |
1910 | 1993 | {#if initialized} |
1911 | | - <div |
1912 | | - class="pane-wrapper" |
1913 | | - class:drop-target-active={dropTargetPane === 'left'} |
1914 | | - style="width: {leftPaneWidthPercent}%" |
1915 | | - bind:this={leftPaneWrapperEl} |
1916 | | - > |
1917 | | - <TabBar |
1918 | | - tabs={getAllTabs(leftTabMgr)} |
1919 | | - activeTabId={leftTabMgr.activeTabId} |
1920 | | - paneId="left" |
1921 | | - maxTabs={MAX_TABS_PER_PANE} |
1922 | | - onTabSwitch={(tabId: TabId) => { |
1923 | | - switchToTab('left', tabId) |
1924 | | - }} |
1925 | | - onTabClose={(tabId: TabId) => { |
1926 | | - handleTabClose('left', tabId) |
1927 | | - }} |
1928 | | - onTabMiddleClick={(tabId: TabId) => { |
1929 | | - handleTabMiddleClick('left', tabId) |
1930 | | - }} |
1931 | | - onNewTab={() => { |
1932 | | - handleNewTab('left') |
1933 | | - }} |
1934 | | - onContextMenu={(tabId: TabId, event: MouseEvent) => { |
1935 | | - handleTabContextMenu('left', tabId, event) |
1936 | | - }} |
1937 | | - onPaneFocus={() => { |
1938 | | - handleFocus('left') |
1939 | | - }} |
1940 | | - /> |
1941 | | - <!--suppress JSUnresolvedReference --> |
1942 | | - {#key getActiveTab(leftTabMgr).id} |
1943 | | - <FilePane |
1944 | | - bind:this={leftPaneRef} |
1945 | | - paneId="left" |
1946 | | - initialPath={leftPath} |
1947 | | - volumeId={leftVolumeId} |
1948 | | - volumePath={leftVolumePath} |
1949 | | - volumeName={leftVolumeName} |
1950 | | - isFocused={focusedPane === 'left'} |
1951 | | - {showHiddenFiles} |
1952 | | - viewMode={leftViewMode} |
1953 | | - sortBy={leftSortBy} |
1954 | | - sortOrder={leftSortOrder} |
1955 | | - directorySortMode={getDirectorySortMode()} |
1956 | | - onPathChange={(path: string) => { |
1957 | | - handlePathChange('left', path) |
1958 | | - }} |
1959 | | - onVolumeChange={(volumeId: string, volumePath: string, targetPath: string) => |
1960 | | - handleVolumeChange('left', volumeId, volumePath, targetPath)} |
1961 | | - onRequestFocus={() => { |
1962 | | - handleFocus('left') |
1963 | | - }} |
1964 | | - onSortChange={(column: SortColumn) => handleSortChange('left', column)} |
1965 | | - onNetworkHostChange={(host: NetworkHost | null) => { |
1966 | | - handleNetworkHostChange('left', host) |
1967 | | - }} |
1968 | | - onCancelLoading={() => { |
1969 | | - handleCancelLoading('left') |
1970 | | - }} |
1971 | | - onMtpFatalError={(msg: string) => handleMtpFatalError('left', msg)} |
1972 | | - /> |
1973 | | - {/key} |
1974 | | - </div> |
| 1994 | + <!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -- Svelte {@render} syntax --> |
| 1995 | + {@render paneBlock('left')} |
1975 | 1996 | <PaneResizer onResize={handlePaneResize} onResizeEnd={handlePaneResizeEnd} onReset={handlePaneResizeReset} /> |
1976 | | - <div |
1977 | | - class="pane-wrapper" |
1978 | | - class:drop-target-active={dropTargetPane === 'right'} |
1979 | | - style="width: {100 - leftPaneWidthPercent}%" |
1980 | | - bind:this={rightPaneWrapperEl} |
1981 | | - > |
1982 | | - <TabBar |
1983 | | - tabs={getAllTabs(rightTabMgr)} |
1984 | | - activeTabId={rightTabMgr.activeTabId} |
1985 | | - paneId="right" |
1986 | | - maxTabs={MAX_TABS_PER_PANE} |
1987 | | - onTabSwitch={(tabId: TabId) => { |
1988 | | - switchToTab('right', tabId) |
1989 | | - }} |
1990 | | - onTabClose={(tabId: TabId) => { |
1991 | | - handleTabClose('right', tabId) |
1992 | | - }} |
1993 | | - onTabMiddleClick={(tabId: TabId) => { |
1994 | | - handleTabMiddleClick('right', tabId) |
1995 | | - }} |
1996 | | - onNewTab={() => { |
1997 | | - handleNewTab('right') |
1998 | | - }} |
1999 | | - onContextMenu={(tabId: TabId, event: MouseEvent) => { |
2000 | | - handleTabContextMenu('right', tabId, event) |
2001 | | - }} |
2002 | | - onPaneFocus={() => { |
2003 | | - handleFocus('right') |
2004 | | - }} |
2005 | | - /> |
2006 | | - <!--suppress JSUnresolvedReference --> |
2007 | | - {#key getActiveTab(rightTabMgr).id} |
2008 | | - <FilePane |
2009 | | - bind:this={rightPaneRef} |
2010 | | - paneId="right" |
2011 | | - initialPath={rightPath} |
2012 | | - volumeId={rightVolumeId} |
2013 | | - volumePath={rightVolumePath} |
2014 | | - volumeName={rightVolumeName} |
2015 | | - isFocused={focusedPane === 'right'} |
2016 | | - {showHiddenFiles} |
2017 | | - viewMode={rightViewMode} |
2018 | | - sortBy={rightSortBy} |
2019 | | - sortOrder={rightSortOrder} |
2020 | | - directorySortMode={getDirectorySortMode()} |
2021 | | - onPathChange={(path: string) => { |
2022 | | - handlePathChange('right', path) |
2023 | | - }} |
2024 | | - onVolumeChange={(volumeId: string, volumePath: string, targetPath: string) => |
2025 | | - handleVolumeChange('right', volumeId, volumePath, targetPath)} |
2026 | | - onRequestFocus={() => { |
2027 | | - handleFocus('right') |
2028 | | - }} |
2029 | | - onSortChange={(column: SortColumn) => handleSortChange('right', column)} |
2030 | | - onNetworkHostChange={(host: NetworkHost | null) => { |
2031 | | - handleNetworkHostChange('right', host) |
2032 | | - }} |
2033 | | - onCancelLoading={() => { |
2034 | | - handleCancelLoading('right') |
2035 | | - }} |
2036 | | - onMtpFatalError={(msg: string) => handleMtpFatalError('right', msg)} |
2037 | | - /> |
2038 | | - {/key} |
2039 | | - </div> |
| 1997 | + <!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -- Svelte {@render} syntax --> |
| 1998 | + {@render paneBlock('right')} |
2040 | 1999 | {:else} |
2041 | 2000 | <LoadingIcon /> |
2042 | 2001 | {/if} |
|
0 commit comments