diff --git a/package-lock.json b/package-lock.json index 70e33952..fdbd6b92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 4e5e978a..a85ccafc 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -22,6 +22,7 @@ import { } from "@/lib/exporter"; import type { ProjectMedia } from "@/lib/recordingSession"; import { matchesShortcut } from "@/lib/shortcuts"; +import { loadUserPreferences, saveUserPreferences } from "@/lib/userPreferences"; import { getAspectRatioValue, getNativeAspectRatioValue, @@ -359,6 +360,28 @@ export default function VideoEditor() { loadInitialData(); }, [applyLoadedProject]); + // Track whether user preferences have been loaded to avoid + // overwriting saved prefs with defaults on the first render + const [prefsHydrated, setPrefsHydrated] = useState(false); + + // Load persisted user preferences on mount (intentionally runs once) + useEffect(() => { + const prefs = loadUserPreferences(); + updateState({ + padding: prefs.padding, + aspectRatio: prefs.aspectRatio, + }); + setExportQuality(prefs.exportQuality); + setExportFormat(prefs.exportFormat); + setPrefsHydrated(true); + }, [updateState]); + + // Auto-save user preferences when settings change + useEffect(() => { + if (!prefsHydrated) return; + saveUserPreferences({ padding, aspectRatio, exportQuality, exportFormat }); + }, [prefsHydrated, padding, aspectRatio, exportQuality, exportFormat]); + const saveProject = useCallback( async (forceSaveAs: boolean) => { if (!videoPath) { diff --git a/src/lib/userPreferences.ts b/src/lib/userPreferences.ts new file mode 100644 index 00000000..e0607880 --- /dev/null +++ b/src/lib/userPreferences.ts @@ -0,0 +1,94 @@ +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; +} + +const DEFAULT_PREFS: UserPreferences = { + padding: 50, + aspectRatio: "16:9", + exportQuality: "good", + exportFormat: "mp4", +}; + +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, + }; +} + +/** + * 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) + } +}