diff --git a/src/Main.tsx b/src/Main.tsx index ab10f01d..7a9417f1 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -16,6 +16,7 @@ import UploadDrawer, { UploadFab } from "./UploadDrawer"; import TextPadDrawer from "./TextPadDrawer"; import { copyPaste, fetchPath } from "./app/transfer"; import { useTransferQueue, useUploadEnqueue } from "./app/transferQueue"; +import { decodeDirectoryHash, encodeDirectoryHash } from "./app/utils"; // Centered helper function Centered({ children }: { children: React.ReactNode }) { @@ -116,7 +117,9 @@ function Main({ search: string; onError: (error: Error) => void; }) { - const [cwd, setCwd] = useState(""); + const [cwd, setCwd] = useState(() => + decodeDirectoryHash(window.location.hash) + ); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [multiSelected, setMultiSelected] = useState(null); @@ -127,6 +130,34 @@ function Main({ const transferQueue = useTransferQueue(); const uploadEnqueue = useUploadEnqueue(); + // Route directory changes through the URL hash so browser history works + const navigateToCwd = useCallback((newCwd: string) => { + const nextHash = encodeDirectoryHash(newCwd); + if (window.location.hash === nextHash) { + setCwd(decodeDirectoryHash(nextHash)); + return; + } + + window.location.hash = nextHash; + }, []); + + // Keep the current directory synchronized with Back, Forward, and reloads + useEffect(() => { + const syncCwdFromHash = () => { + const nextCwd = decodeDirectoryHash(window.location.hash); + const normalizedHash = encodeDirectoryHash(nextCwd); + if (window.location.hash !== normalizedHash) { + window.history.replaceState(null, "", normalizedHash); + } + + setCwd(nextCwd); + }; + + syncCwdFromHash(); + window.addEventListener("hashchange", syncCwdFromHash); + return () => window.removeEventListener("hashchange", syncCwdFromHash); + }, []); + const fetchFiles = useCallback(() => { fetchPath(cwd) .then((files) => { @@ -178,7 +209,7 @@ function Main({ return ( <> - {cwd && } + {cwd && } {loading ? ( @@ -194,7 +225,7 @@ function Main({ > setCwd(newCwd)} + onCwdChange={navigateToCwd} multiSelected={multiSelected} onMultiSelect={handleMultiSelect} emptyMessage={No files or folders} diff --git a/src/app/utils.ts b/src/app/utils.ts index 038c3ef1..e8a0411b 100644 --- a/src/app/utils.ts +++ b/src/app/utils.ts @@ -7,3 +7,37 @@ export function humanReadableSize(size: number) { } return `${size.toFixed(1)} ${units[i]}`; } + +// Encode each directory segment while preserving slash separators +function encodePathSegments(path: string) { + return path.split("/").map(encodeURIComponent).join("/"); +} + +// Keep directory keys in the same shape used by WebDAV browsing state +function normalizeDirectoryPath(path: string) { + const normalizedPath = path.replace(/^\/+/, ""); + if (!normalizedPath) return ""; + return normalizedPath.endsWith("/") ? normalizedPath : `${normalizedPath}/`; +} + +// Convert a directory key into the app hash route +export function encodeDirectoryHash(cwd: string) { + return `#/${encodePathSegments(normalizeDirectoryPath(cwd))}`; +} + +// Convert the app hash route back into a directory key +export function decodeDirectoryHash(hash: string) { + if (!hash || hash === "#" || !hash.startsWith("#/")) return ""; + + try { + return normalizeDirectoryPath( + hash + .slice(2) + .split("/") + .map((segment) => decodeURIComponent(segment)) + .join("/") + ); + } catch { + return ""; + } +}