Skip to content

Commit bf462e9

Browse files
committed
Fix Back/Forward navi
Even across the Network screens
1 parent dbeebaf commit bf462e9

5 files changed

Lines changed: 336 additions & 93 deletions

File tree

docs/features/back-forward-navigation.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Navigate between previously visited folders using browser-style back/forward fun
66

77
- **Per-pane history**: Each pane maintains its own independent back/forward stack
88
- **Session-only**: History resets when quitting the app (not persisted)
9+
- **Cross-volume navigation**: History tracks navigation across different volumes, so you can go back/forward between
10+
volumes (e.g., from an external drive back to Macintosh HD)
11+
- **Network volume support**: When navigating to/from the Network virtual volume, history is preserved
912
- **Deleted folders**: When navigating to a folder that no longer exists, walks up the parent tree until finding an
1013
existing folder. If the entire volume is gone, skips to the next history entry.
1114
- **History preservation**: Skipped entries remain in history (folder may become available again)

src/lib/file-explorer/DualPaneExplorer.svelte

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@
2222
DEFAULT_VOLUME_ID,
2323
type UnlistenFn,
2424
} from '$lib/tauri-commands'
25-
import type { VolumeInfo, SortColumn, SortOrder } from './types'
25+
import type { VolumeInfo, SortColumn, SortOrder, NetworkHost } from './types'
2626
import { defaultSortOrders, DEFAULT_SORT_BY } from './types'
2727
import { ensureFontMetricsLoaded } from '$lib/font-metrics'
2828
import {
2929
createHistory,
3030
push,
31+
pushPath,
3132
back,
3233
forward,
33-
getCurrentPath,
34+
getCurrentEntry,
3435
canGoBack,
3536
canGoForward,
3637
type NavigationHistory,
@@ -64,8 +65,9 @@
6465
let unlistenNavigation: UnlistenFn | undefined
6566
6667
// Navigation history for each pane (per-pane, session-only)
67-
let leftHistory = $state<NavigationHistory>(createHistory('~'))
68-
let rightHistory = $state<NavigationHistory>(createHistory('~'))
68+
// Initialize with default volume - will be updated on mount with actual state
69+
let leftHistory = $state<NavigationHistory>(createHistory(DEFAULT_VOLUME_ID, '~'))
70+
let rightHistory = $state<NavigationHistory>(createHistory(DEFAULT_VOLUME_ID, '~'))
6971
7072
// Derived volume paths - handle 'network' virtual volume specially
7173
const leftVolumePath = $derived(
@@ -77,7 +79,8 @@
7779
7880
function handleLeftPathChange(path: string) {
7981
leftPath = path
80-
leftHistory = push(leftHistory, path)
82+
// Use pushPath to keep current volumeId (directory navigation within volume)
83+
leftHistory = pushPath(leftHistory, path)
8184
void saveAppStatus({ leftPath: path })
8285
void saveLastUsedPathForVolume(leftVolumeId, path)
8386
// Re-focus to maintain keyboard handling after navigation
@@ -86,13 +89,35 @@
8689
8790
function handleRightPathChange(path: string) {
8891
rightPath = path
89-
rightHistory = push(rightHistory, path)
92+
// Use pushPath to keep current volumeId (directory navigation within volume)
93+
rightHistory = pushPath(rightHistory, path)
9094
void saveAppStatus({ rightPath: path })
9195
void saveLastUsedPathForVolume(rightVolumeId, path)
9296
// Re-focus to maintain keyboard handling after navigation
9397
containerElement?.focus()
9498
}
9599
100+
// Handle network host selection changes (for history tracking)
101+
function handleLeftNetworkHostChange(host: NetworkHost | null) {
102+
// Push to history with network host state
103+
leftHistory = push(leftHistory, {
104+
volumeId: 'network',
105+
path: 'smb://',
106+
networkHost: host ?? undefined,
107+
})
108+
containerElement?.focus()
109+
}
110+
111+
function handleRightNetworkHostChange(host: NetworkHost | null) {
112+
// Push to history with network host state
113+
rightHistory = push(rightHistory, {
114+
volumeId: 'network',
115+
path: 'smb://',
116+
networkHost: host ?? undefined,
117+
})
118+
containerElement?.focus()
119+
}
120+
96121
/**
97122
* Handles sorting column click for left pane.
98123
* If clicking the same column, toggles order. Otherwise, switches to new column with its default order.
@@ -199,6 +224,9 @@
199224
leftVolumeId = volumeId
200225
leftPath = pathToNavigate
201226
227+
// Push volume change to history (this enables back/forward across volumes)
228+
leftHistory = push(leftHistory, { volumeId, path: pathToNavigate })
229+
202230
// Focus the left pane after successful volume selection
203231
focusedPane = 'left'
204232
void saveAppStatus({ leftVolumeId: volumeId, leftPath: pathToNavigate, focusedPane: 'left' })
@@ -222,6 +250,9 @@
222250
rightVolumeId = volumeId
223251
rightPath = pathToNavigate
224252
253+
// Push volume change to history (this enables back/forward across volumes)
254+
rightHistory = push(rightHistory, { volumeId, path: pathToNavigate })
255+
225256
// Focus the right pane after successful volume selection
226257
focusedPane = 'right'
227258
void saveAppStatus({ rightVolumeId: volumeId, rightPath: pathToNavigate, focusedPane: 'right' })
@@ -387,9 +418,9 @@
387418
rightVolumeId = rightContaining?.id ?? defaultId
388419
}
389420
390-
// Initialize history with loaded paths
391-
leftHistory = createHistory(status.leftPath)
392-
rightHistory = createHistory(status.rightPath)
421+
// Initialize history with loaded paths and their volumes
422+
leftHistory = createHistory(leftVolumeId, status.leftPath)
423+
rightHistory = createHistory(rightVolumeId, status.rightPath)
393424
394425
initialized = true
395426
@@ -485,20 +516,43 @@
485516
}
486517
487518
/**
488-
* Updates pane state after navigating back/forward (doesn't push to history).
519+
* Updates pane state after navigating back/forward (restores full state from history entry).
520+
* This includes both path AND volumeId changes - enabling back/forward across volumes.
489521
*/
490522
function updatePaneAfterHistoryNavigation(isLeft: boolean, newHistory: NavigationHistory, targetPath: string) {
523+
const entry = getCurrentEntry(newHistory)
524+
const paneRef = isLeft ? leftPaneRef : rightPaneRef
525+
491526
if (isLeft) {
492527
leftHistory = newHistory
493528
leftPath = targetPath
494-
void saveAppStatus({ leftPath: targetPath })
495-
void saveLastUsedPathForVolume(leftVolumeId, targetPath)
529+
// Restore volume context if it changed
530+
if (entry.volumeId !== leftVolumeId) {
531+
leftVolumeId = entry.volumeId
532+
void saveAppStatus({ leftVolumeId: entry.volumeId, leftPath: targetPath })
533+
} else {
534+
void saveAppStatus({ leftPath: targetPath })
535+
}
536+
void saveLastUsedPathForVolume(entry.volumeId, targetPath)
496537
} else {
497538
rightHistory = newHistory
498539
rightPath = targetPath
499-
void saveAppStatus({ rightPath: targetPath })
500-
void saveLastUsedPathForVolume(rightVolumeId, targetPath)
540+
// Restore volume context if it changed
541+
if (entry.volumeId !== rightVolumeId) {
542+
rightVolumeId = entry.volumeId
543+
void saveAppStatus({ rightVolumeId: entry.volumeId, rightPath: targetPath })
544+
} else {
545+
void saveAppStatus({ rightPath: targetPath })
546+
}
547+
void saveLastUsedPathForVolume(entry.volumeId, targetPath)
548+
}
549+
550+
// Restore network host state if navigating within network volume
551+
if (entry.volumeId === 'network') {
552+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
553+
paneRef?.setNetworkHost?.(entry.networkHost ?? null)
501554
}
555+
502556
containerElement?.focus()
503557
}
504558
@@ -526,9 +580,20 @@
526580
return
527581
}
528582
529-
const targetPath = await resolveValidPath(getCurrentPath(newHistory))
530-
if (targetPath !== null) {
531-
updatePaneAfterHistoryNavigation(isLeft, newHistory, targetPath)
583+
// Get the target entry (includes volumeId, path, and network state)
584+
const targetEntry = getCurrentEntry(newHistory)
585+
586+
// For network virtual volume, path resolution doesn't apply
587+
// (network browser handles its own state)
588+
if (targetEntry.volumeId === 'network') {
589+
updatePaneAfterHistoryNavigation(isLeft, newHistory, targetEntry.path)
590+
return
591+
}
592+
593+
// For real volumes, resolve path to handle deleted folders
594+
const resolvedPath = await resolveValidPath(targetEntry.path)
595+
if (resolvedPath !== null) {
596+
updatePaneAfterHistoryNavigation(isLeft, newHistory, resolvedPath)
532597
}
533598
}
534599
@@ -573,6 +638,7 @@
573638
onVolumeChange={handleLeftVolumeChange}
574639
onRequestFocus={handleLeftFocus}
575640
onSortChange={handleLeftSortChange}
641+
onNetworkHostChange={handleLeftNetworkHostChange}
576642
/>
577643
<FilePane
578644
bind:this={rightPaneRef}
@@ -588,6 +654,7 @@
588654
onVolumeChange={handleRightVolumeChange}
589655
onRequestFocus={handleRightFocus}
590656
onSortChange={handleRightSortChange}
657+
onNetworkHostChange={handleRightNetworkHostChange}
591658
/>
592659
{:else}
593660
<LoadingIcon />

src/lib/file-explorer/FilePane.svelte

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
onVolumeChange?: (volumeId: string, volumePath: string, targetPath: string) => void
5252
onSortChange?: (column: SortColumn) => void
5353
onRequestFocus?: () => void
54+
/** Called when network host selection changes (for history tracking) */
55+
onNetworkHostChange?: (host: NetworkHost | null) => void
5456
}
5557
5658
const {
@@ -66,6 +68,7 @@
6668
onVolumeChange,
6769
onSortChange,
6870
onRequestFocus,
71+
onNetworkHostChange,
6972
}: Props = $props()
7073
7174
let currentPath = $state(untrack(() => initialPath))
@@ -136,6 +139,18 @@
136139
cacheGeneration++
137140
}
138141
142+
// Set network host state (for history navigation)
143+
export function setNetworkHost(host: NetworkHost | null): void {
144+
selectedNetworkHost = host
145+
mountError = null
146+
lastMountAttempt = null
147+
}
148+
149+
// Get current network host (for history tracking)
150+
export function getNetworkHost(): NetworkHost | null {
151+
return selectedNetworkHost
152+
}
153+
139154
// Navigate to parent directory, selecting the folder we came from
140155
export async function navigateToParent(): Promise<boolean> {
141156
if (currentPath === '/' || currentPath === volumePath) {
@@ -366,13 +381,15 @@
366381
// Handle network host selection - show the ShareBrowser
367382
function handleNetworkHostSelect(host: NetworkHost) {
368383
selectedNetworkHost = host
384+
onNetworkHostChange?.(host)
369385
}
370386
371387
// Handle going back from ShareBrowser to network host list
372388
function handleNetworkBack() {
373389
selectedNetworkHost = null
374390
mountError = null
375391
lastMountAttempt = null
392+
onNetworkHostChange?.(null)
376393
}
377394
378395
// Handle going back from mount error to share list

0 commit comments

Comments
 (0)