import { DEFAULT_EDITOR_LAYOUT_SETTINGS, DEFAULT_EXPORT_SETTINGS, } from "@/components/video-editor/editorDefaults"; import type { ExportFormat, ExportQuality } from "@/lib/exporter"; import type { AspectRatio } from "@/utils/aspectRatioUtils"; const PREFS_KEY = "openscreen_user_preferences"; const VALID_ASPECT_RATIOS: readonly string[] = [ "16:9", "9:16", "1:1", "4:3", "4:5", "16:10", "10:16", "native", ]; export interface UserPreferences { /** Default padding % */ padding: number; /** Default aspect ratio */ aspectRatio: AspectRatio; /** Default export quality */ exportQuality: ExportQuality; /** Default export format */ exportFormat: ExportFormat; /** Folder used for the most recent successful export, if any */ exportFolder: string | null; } export const DEFAULT_PREFS: UserPreferences = { padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio, exportQuality: DEFAULT_EXPORT_SETTINGS.quality, exportFormat: DEFAULT_EXPORT_SETTINGS.format, exportFolder: null, }; function safeJsonParse(text: string | null): Record | null { if (!text) return null; try { return JSON.parse(text); } catch { return null; } } /** * Load persisted user preferences from localStorage. * Returns defaults for any missing or invalid fields. */ export function loadUserPreferences(): UserPreferences { let raw: Record | null = null; try { raw = safeJsonParse(localStorage.getItem(PREFS_KEY)); } catch { return { ...DEFAULT_PREFS }; } if (!raw || typeof raw !== "object") return { ...DEFAULT_PREFS }; return { padding: typeof raw.padding === "number" && Number.isFinite(raw.padding) && raw.padding >= 0 && raw.padding <= 100 ? raw.padding : DEFAULT_PREFS.padding, aspectRatio: typeof raw.aspectRatio === "string" && VALID_ASPECT_RATIOS.includes(raw.aspectRatio) ? (raw.aspectRatio as AspectRatio) : DEFAULT_PREFS.aspectRatio, exportQuality: raw.exportQuality === "medium" || raw.exportQuality === "good" || raw.exportQuality === "source" ? (raw.exportQuality as ExportQuality) : DEFAULT_PREFS.exportQuality, exportFormat: raw.exportFormat === "gif" || raw.exportFormat === "mp4" ? (raw.exportFormat as ExportFormat) : DEFAULT_PREFS.exportFormat, exportFolder: typeof raw.exportFolder === "string" && raw.exportFolder.length > 0 ? raw.exportFolder : DEFAULT_PREFS.exportFolder, }; } /** * Extracts the parent directory from a saved file path. Handles both POSIX * and Windows separators since the path comes from the OS save dialog. * * Root directories are preserved with their trailing separator so that the * value is still a valid directory path: * "/video.mp4" -> "/" * "C:\\video.mp4" -> "C:\\" * * Returns null if no separator is found. */ export function parentDirectoryOf(filePath: string): string | null { const lastSep = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\")); if (lastSep < 0) return null; // POSIX root, e.g. "/video.mp4" -> "/" if (lastSep === 0) return filePath[0]; // Windows drive root, e.g. "C:\\video.mp4" -> "C:\\" if (lastSep === 2 && /^[A-Za-z]:[/\\]/.test(filePath)) { return filePath.slice(0, lastSep + 1); } return filePath.slice(0, lastSep); } /** * Returns the remembered export folder as `string | undefined`, suitable for * passing directly to IPC handlers that treat absence as "use the default". */ export function getExportFolder(): string | undefined { return loadUserPreferences().exportFolder ?? undefined; } /** * Persist user preferences to localStorage. * Only the explicitly provided fields are updated. */ export function saveUserPreferences(partial: Partial): void { const current = loadUserPreferences(); const merged = { ...current, ...partial }; try { localStorage.setItem(PREFS_KEY, JSON.stringify(merged)); } catch { // localStorage may be unavailable (e.g. private browsing quota exceeded) } }