import { createElement, type ReactNode } from "react"; import { Folder, HardDrive, Home, StarFilled, Usb } from "@/components/icons/platform-icons"; import { OsIcon } from "@/components/icons/OsIcon"; import { DEVICE_ICONS, FILE_SIDEBAR_ICONS, FOLDER_ICONS } from "@/components/icons/icon-assets"; import type { FileListEntry, FileReadResponse } from "@/lib/shared/contracts/files"; import type { FileManagerSidebarSection, RemovableSidebarItem } from "@/modules/files/components/chrome/file-manager-sidebar"; import type { UsbDrive } from "@/lib/shared/contracts/usb"; import type { GoogleDriveConnection } from "@/modules/files/hooks/useGoogleDrive"; import { normalizePathForDisplay, toUiFileEntry, type FileEntry, } from "@/modules/files/components/file-manager-presenters"; import type { SortBy } from "@/modules/files/components/manager/file-manager-state"; const BYTES_PER_TB = 1024 ** 4; const BYTES_PER_GB = 1024 ** 3; const BYTES_PER_MB = 1024 ** 2; export const STARRED_VIRTUAL_PATH = ["⭐Starred"] as const; export const DRIVE_PATH_PREFIX = "Drive" as const; function createKoraIcon(src: string, fallback: ReactNode) { return createElement(OsIcon, { src, className: "size-4 object-contain", fallback, }); } export const sidebarSections: FileManagerSidebarSection[] = [ { title: "Favorites", items: [ { name: "Home", icon: createKoraIcon( FILE_SIDEBAR_ICONS.home, createElement(Home, { className: "size-4 text-muted-foreground" }), ), path: [], }, { name: "Starred", icon: createKoraIcon( FILE_SIDEBAR_ICONS.starred, createElement(StarFilled, { className: "size-4 text-amber-400" }), ), path: [...STARRED_VIRTUAL_PATH], }, { name: "Documents", icon: createKoraIcon( FOLDER_ICONS.document, createElement(Folder, { className: "size-4 text-sky-400" }), ), path: ["Documents"], }, { name: "Downloads", icon: createKoraIcon( FOLDER_ICONS.download, createElement(Folder, { className: "size-4 text-emerald-400" }), ), path: ["Downloads"], }, { name: "Media", icon: createKoraIcon( FOLDER_ICONS.video, createElement(Folder, { className: "size-4 text-amber-400" }), ), path: ["Media"], }, { name: "Apps", icon: createKoraIcon( FOLDER_ICONS.app, createElement(Folder, { className: "size-4 text-violet-400" }), ), path: ["AppData"], }, ], }, { title: "Locations", items: [], }, { title: "Cloud", items: [], }, ]; export function formatStorageValue(bytes: number): string { if (bytes >= BYTES_PER_TB) { const value = (bytes / BYTES_PER_TB).toFixed(1); return `${value.endsWith(".0") ? value.slice(0, -2) : value} TB`; } if (bytes >= BYTES_PER_GB) { const value = (bytes / BYTES_PER_GB).toFixed(1); return `${value.endsWith(".0") ? value.slice(0, -2) : value} GB`; } return `${(bytes / BYTES_PER_MB).toFixed(0)} MB`; } export function clampPercent(value: number | null | undefined) { if (typeof value !== "number" || Number.isNaN(value)) return 0; return Math.max(0, Math.min(value, 100)); } export function sortEntries({ currentEntries, isGlobalSearchActive, searchQuery, sortBy, sortDir, }: { currentEntries: FileEntry[]; isGlobalSearchActive: boolean; searchQuery: string; sortBy: SortBy; sortDir: "asc" | "desc"; }) { let entries = [...currentEntries]; if (searchQuery && !isGlobalSearchActive) { const normalized = searchQuery.toLowerCase(); entries = entries.filter((entry) => entry.name.toLowerCase().includes(normalized)); } const folders = entries.filter((entry) => entry.type === "folder"); const files = entries.filter((entry) => entry.type === "file"); const dir = sortDir === "asc" ? 1 : -1; const sortFn = (left: FileEntry, right: FileEntry) => { if (sortBy === "name") return dir * left.name.localeCompare(right.name); if (sortBy === "modified") { return dir * (new Date(left.modifiedAt).getTime() - new Date(right.modifiedAt).getTime()); } return dir * ((left.sizeBytes ?? 0) - (right.sizeBytes ?? 0)); }; return [...folders.sort(sortFn), ...files.sort(sortFn)]; } export function getCurrentEntries({ directoryEntries, globalEntries, isGlobalSearchActive, isStarredView, starredEntries, }: { directoryEntries: FileListEntry[] | undefined; globalEntries: FileListEntry[] | undefined; isGlobalSearchActive: boolean; isStarredView: boolean; starredEntries: FileListEntry[] | undefined; }) { if (isGlobalSearchActive) { return (globalEntries ?? []).map(toUiFileEntry); } const rawEntries = isStarredView ? starredEntries ?? [] : directoryEntries ?? []; return rawEntries.map(toUiFileEntry); } export function getOpenFileDetails({ fileDrafts, filePath, language, viewer, }: { fileDrafts: Record; filePath: string | null; language: string; viewer: FileReadResponse | null; }) { const content = filePath && viewer?.mode === "text" ? (fileDrafts[filePath] ?? viewer.content ?? "") : ""; const badgeLabel = viewer ? viewer.mode === "text" ? language : viewer.mode.replaceAll("_", " ") : language; const hasUnsavedChanges = filePath !== null && viewer?.mode === "text" && fileDrafts[filePath] !== undefined && fileDrafts[filePath] !== (viewer.content ?? ""); return { badgeLabel, content, hasUnsavedChanges }; } export function getStorageSummary( storage: { totalBytes: number; usedBytes: number; usedPercent: number } | undefined, ) { if (!storage || storage.totalBytes <= 0) { return { percent: 0, text: "-- / --" }; } return { percent: clampPercent(storage.usedPercent), text: `${formatStorageValue(storage.usedBytes)} / ${formatStorageValue(storage.totalBytes)}`, }; } export function getViewFlags(currentPath: string[]) { return { isSharedView: currentPath[0] === "Shared", isStarredView: currentPath.length === 1 && currentPath[0] === STARRED_VIRTUAL_PATH[0], isTrashView: currentPath[0] === "Trash", isDriveView: currentPath[0] === DRIVE_PATH_PREFIX && Boolean(currentPath[1]), }; } export function getDriveItems( connections: GoogleDriveConnection[] | undefined, ): Array<{ id: string; name: string; email: string; path: string[] }> { return (connections ?? []).map((conn) => ({ id: conn.id, name: conn.displayName ?? conn.email, email: conn.email, path: [DRIVE_PATH_PREFIX, conn.id], })); } export function getLocationItems( shares: Array<{ mountPath: string }> | undefined, ) { const hosts = new Map(); for (const share of shares ?? []) { const segments = share.mountPath.split("/").filter(Boolean); const hostSegment = segments[1]; const rootSegment = segments[0]; if (!hostSegment || !rootSegment) continue; hosts.set(hostSegment, [rootSegment, hostSegment]); } return Array.from(hosts.entries()) .sort(([left], [right]) => left.localeCompare(right)) .map(([host, hostPath]) => ({ name: host, icon: createKoraIcon( DEVICE_ICONS.network, createElement(HardDrive, { className: "size-4 text-cyan-400" }), ), path: hostPath, })); } function usbPathSegment(label: string, id: string): string { return label.replace(/[^a-zA-Z0-9\-_]/g, "_").replace(/_+/g, "_").slice(0, 32) || id.slice(0, 8); } export function getRemovableItems(drives: UsbDrive[]): RemovableSidebarItem[] { return drives.map((drive) => ({ name: drive.label, icon: createKoraIcon( DEVICE_ICONS.usb, createElement(Usb, { className: "size-4 text-amber-400" }), ), path: ["Removable", usbPathSegment(drive.label, drive.id)], driveId: drive.id, isMounted: drive.isMounted, })); } export function getLocalSharesByPath( shares: | Array<{ id: string; shareName: string; sourcePath: string; sharedPath: string; isMounted: boolean; isExported: boolean; }> | undefined, ) { const map = new Map< string, { id: string; shareName: string; sourcePath: string; sharedPath: string; isMounted: boolean; isExported: boolean; } >(); for (const share of shares ?? []) { map.set(share.sourcePath, share); map.set(share.sharedPath, share); } return map; } export function getBrowserCounts(entries: FileEntry[]) { return { fileCount: entries.filter((entry) => entry.type === "file").length, folderCount: entries.filter((entry) => entry.type === "folder").length, }; } export function getClipboardDisplayName( clipboardState: { names: string[] } | null, ) { if (!clipboardState) return null; return clipboardState.names.length === 1 ? clipboardState.names[0] : `${clipboardState.names.length} items`; } export function getCurrentPathForDisplay(currentPath: string[]) { return normalizePathForDisplay(currentPath); } export function validateEntryName(name: string): string | null { if (name.includes("/") || name.includes("\0")) { return 'Name cannot contain "/" or null characters'; } return null; }