export type DriveView = "files" | "recent" | "starred" | "trash" | "search" | "shared" /** Decode one URL path segment (safe for names with literal `%`). */ export function decodePathSegment(segment: string): string { try { return decodeURIComponent(segment.replace(/\+/g, " ")) } catch { return segment } } function decodePathSegments(segments: string[]): string[] { return segments.map(decodePathSegment) } export function encodePathSegments(segments: string[]): string { return segments.map((s) => encodeURIComponent(decodePathSegment(s))).join("/") } export function buildDriveFolderHref( view: Extract, segments: string[] ): string { const decoded = decodePathSegments(segments) if (decoded.length === 0) { return view === "shared" ? "/drive/shared" : "/drive" } const prefix = view === "shared" ? "/drive/shared/folders" : "/drive/folders" return `${prefix}/${encodePathSegments(decoded)}` } export interface DriveRouteState { view: DriveView pathSegments: string[] page: number fileId: string | null query: string } export function parseDriveSegments(segments: string[] | undefined): DriveRouteState { const parts = segments ?? [] if (parts.length === 0) { return { view: "files", pathSegments: [], page: 1, fileId: null, query: "" } } const head = parts[0] if (head === "recent" || head === "starred" || head === "trash") { return { view: head, pathSegments: [], page: 1, fileId: null, query: "" } } if (head === "shared") { const folderParts = parts.slice(1) let page = 1 if (folderParts[0] === "folders") { folderParts.shift() const pageIdx = folderParts.indexOf("page") if (pageIdx >= 0 && folderParts[pageIdx + 1]) { page = Math.max(1, parseInt(folderParts[pageIdx + 1], 10) || 1) folderParts.splice(pageIdx, 2) } return { view: "shared", pathSegments: decodePathSegments(folderParts), page, fileId: null, query: "", } } return { view: "shared", pathSegments: [], page: 1, fileId: null, query: "" } } if (head === "search") { return { view: "search", pathSegments: [], page: 1, fileId: null, query: "" } } if (head === "edit" && parts[1]) { return { view: "files", pathSegments: [], page: 1, fileId: decodeURIComponent(parts[1]), query: "", } } if (head === "folders") { const folderParts = parts.slice(1) let page = 1 const pageIdx = folderParts.indexOf("page") if (pageIdx >= 0 && folderParts[pageIdx + 1]) { page = Math.max(1, parseInt(folderParts[pageIdx + 1], 10) || 1) folderParts.splice(pageIdx, 2) } return { view: "files", pathSegments: decodePathSegments(folderParts), page, fileId: null, query: "", } } return { view: "files", pathSegments: decodePathSegments(parts), page: 1, fileId: null, query: "", } } export function buildDrivePath(state: DriveRouteState): string { if (state.view === "recent") return "/drive/recent" if (state.view === "starred") return "/drive/starred" if (state.view === "trash") return "/drive/trash" if (state.view === "search") return "/drive/search" if (state.view === "shared") { const folder = state.pathSegments.length ? `/drive/shared/folders/${encodePathSegments(state.pathSegments)}` : "/drive/shared" const pageSuffix = state.page > 1 ? `/page/${state.page}` : "" return `${folder}${pageSuffix}` } if (state.fileId && state.view === "files") { return `/drive/edit/${encodeURIComponent(state.fileId)}` } const folder = state.pathSegments.length ? `/drive/folders/${encodePathSegments(state.pathSegments)}` : "/drive" const pageSuffix = state.page > 1 ? `/page/${state.page}` : "" return `${folder}${pageSuffix}` } export function folderPathFromSegments(segments: string[]): string { const decoded = decodePathSegments(segments) if (decoded.length === 0) return "/" return "/" + decoded.join("/") } export function parentFolderPathFromFilePath(filePath: string): string { const normalized = filePath.replace(/^\/+/, "").replace(/\/+$/, "") if (!normalized) return "/" const parts = normalized.split("/") if (parts.length <= 1) return "/" return "/" + parts.slice(0, -1).join("/") } function isSafeDriveReturnPath(path: string): boolean { if (!path.startsWith("/drive")) return false if (path.startsWith("//")) return false if (path.includes("://")) return false return true } /** Stable Nextcloud file id segment (numeric). */ export function isDriveFileIdSegment(value: string): boolean { return /^[1-9][0-9]*$/.test(value) } /** Rich-text editor URL — id-only, no returnTo query param. */ export function buildDriveDocsEditHref(fileId: string | number): string { return `/drive/docs/${String(fileId)}/edit` } export function buildDriveEditHref(filePath: string, returnTo?: string): string { const params = new URLSearchParams() if (returnTo && isSafeDriveReturnPath(returnTo)) { params.set("returnTo", returnTo) } const base = `/drive/edit/${encodeURIComponent(filePath)}` const qs = params.toString() return qs ? `${base}?${qs}` : base } /** Resolve back link from editor: prefer session returnTo, else explicit returnTo, else parent folder. */ export function resolveDriveEditReturnTo( returnTo: string | null | undefined, filePath: string, folderHref: (folderPath: string) => string, sessionReturnTo?: string | null ): string { if (sessionReturnTo && isSafeDriveReturnPath(sessionReturnTo)) return sessionReturnTo if (returnTo && isSafeDriveReturnPath(returnTo)) return returnTo return folderHref(parentFolderPathFromFilePath(filePath)) }