From d5f59a7b8e1fa49590a26babd04518a39f406ce0 Mon Sep 17 00:00:00 2001 From: JasonOA888 Date: Sat, 4 Apr 2026 23:16:39 +0800 Subject: [PATCH 1/4] fix: persist user settings across sessions Add userPreferences module to save/load padding, aspect ratio, export format and quality to localStorage. Applied on mount in VideoEditor. Closes #306 --- src/components/video-editor/VideoEditor.tsx | 1 + src/lib/userPreferences.ts | 69 +++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/lib/userPreferences.ts diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 4e5e978a..e2e34f16 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -21,6 +21,7 @@ import { VideoExporter, } from "@/lib/exporter"; import type { ProjectMedia } from "@/lib/recordingSession"; +import { loadUserPreferences, saveUserPreferences } from "@/lib/userPreferences"; import { matchesShortcut } from "@/lib/shortcuts"; import { getAspectRatioValue, diff --git a/src/lib/userPreferences.ts b/src/lib/userPreferences.ts new file mode 100644 index 00000000..ae9d14f5 --- /dev/null +++ b/src/lib/userPreferences.ts @@ -0,0 +1,69 @@ +import type { ExportFormat, ExportQuality } from "@/lib/exporter"; +import type { AspectRatio } from "@/utils/aspectRatioUtils"; + +const PREFS_KEY = "openscreen_user_preferences"; + +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 { + const raw = safeJsonParse(localStorage.getItem(PREFS_KEY)); + 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" ? (raw.aspectRatio as AspectRatio) : DEFAULT_PREFS.aspectRatio, + exportQuality: + raw.exportQuality === "medium" || raw.exportQuality === "source" + ? (raw.exportQuality as ExportQuality) + : DEFAULT_PREFS.exportQuality, + exportFormat: + raw.exportFormat === "gif" ? (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) + } +} From 7d746196d2c26e4e6f742177fa3e6861940b50b3 Mon Sep 17 00:00:00 2001 From: JasonOA888 Date: Sat, 4 Apr 2026 23:27:56 +0800 Subject: [PATCH 2/4] fix: persist user settings across sessions (closes #306) Load saved preferences (padding, aspect ratio, export quality, export format) on mount and auto-save whenever these settings change. Uses the existing userPreferences.ts utility with a ref guard to prevent overwriting saved prefs with defaults before the initial load completes. --- src/components/video-editor/VideoEditor.tsx | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index e2e34f16..6d7c5c56 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -21,8 +21,8 @@ import { VideoExporter, } from "@/lib/exporter"; import type { ProjectMedia } from "@/lib/recordingSession"; -import { loadUserPreferences, saveUserPreferences } from "@/lib/userPreferences"; import { matchesShortcut } from "@/lib/shortcuts"; +import { loadUserPreferences, saveUserPreferences } from "@/lib/userPreferences"; import { getAspectRatioValue, getNativeAspectRatioValue, @@ -360,6 +360,30 @@ 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 prefsLoadedRef = useRef(false); + + // Load persisted user preferences on mount + useEffect(() => { + const prefs = loadUserPreferences(); + updateState({ + padding: prefs.padding, + aspectRatio: prefs.aspectRatio, + }); + setExportQuality(prefs.exportQuality); + setExportFormat(prefs.exportFormat); + prefsLoadedRef.current = true; + // We intentionally only want this to run once on mount + // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect + }, []); + + // Auto-save user preferences when settings change + useEffect(() => { + if (!prefsLoadedRef.current) return; + saveUserPreferences({ padding, aspectRatio, exportQuality, exportFormat }); + }, [padding, aspectRatio, exportQuality, exportFormat]); + const saveProject = useCallback( async (forceSaveAs: boolean) => { if (!videoPath) { From 4f48ecd4bc796a43fc16c34f0f7acad400345ac2 Mon Sep 17 00:00:00 2001 From: JasonOA888 Date: Sat, 4 Apr 2026 23:58:25 +0800 Subject: [PATCH 3/4] fix: address code review feedback for settings persistence - Replace useRef with useState for prefsHydrated to prevent race condition - Wrap localStorage.getItem in try/catch in loadUserPreferences - Validate aspectRatio against known valid values - Include 'good' in exportQuality validation, 'mp4' in exportFormat validation --- package-lock.json | 4 +-- src/components/video-editor/VideoEditor.tsx | 8 +++--- src/lib/userPreferences.ts | 28 ++++++++++++++++++--- 3 files changed, 30 insertions(+), 10 deletions(-) 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 6d7c5c56..4168ef8c 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -362,7 +362,7 @@ export default function VideoEditor() { // Track whether user preferences have been loaded to avoid // overwriting saved prefs with defaults on the first render - const prefsLoadedRef = useRef(false); + const [prefsHydrated, setPrefsHydrated] = useState(false); // Load persisted user preferences on mount useEffect(() => { @@ -373,16 +373,16 @@ export default function VideoEditor() { }); setExportQuality(prefs.exportQuality); setExportFormat(prefs.exportFormat); - prefsLoadedRef.current = true; + setPrefsHydrated(true); // We intentionally only want this to run once on mount // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect }, []); // Auto-save user preferences when settings change useEffect(() => { - if (!prefsLoadedRef.current) return; + if (!prefsHydrated) return; saveUserPreferences({ padding, aspectRatio, exportQuality, exportFormat }); - }, [padding, aspectRatio, exportQuality, exportFormat]); + }, [prefsHydrated, padding, aspectRatio, exportQuality, exportFormat]); const saveProject = useCallback( async (forceSaveAs: boolean) => { diff --git a/src/lib/userPreferences.ts b/src/lib/userPreferences.ts index ae9d14f5..58397996 100644 --- a/src/lib/userPreferences.ts +++ b/src/lib/userPreferences.ts @@ -3,6 +3,17 @@ 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; @@ -35,7 +46,12 @@ function safeJsonParse(text: string | null): Record | null { * Returns defaults for any missing or invalid fields. */ export function loadUserPreferences(): UserPreferences { - const raw = safeJsonParse(localStorage.getItem(PREFS_KEY)); + 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 { @@ -44,13 +60,17 @@ export function loadUserPreferences(): UserPreferences { ? raw.padding : DEFAULT_PREFS.padding, aspectRatio: - typeof raw.aspectRatio === "string" ? (raw.aspectRatio as AspectRatio) : DEFAULT_PREFS.aspectRatio, + typeof raw.aspectRatio === "string" && VALID_ASPECT_RATIOS.includes(raw.aspectRatio) + ? (raw.aspectRatio as AspectRatio) + : DEFAULT_PREFS.aspectRatio, exportQuality: - raw.exportQuality === "medium" || raw.exportQuality === "source" + raw.exportQuality === "medium" || raw.exportQuality === "good" || raw.exportQuality === "source" ? (raw.exportQuality as ExportQuality) : DEFAULT_PREFS.exportQuality, exportFormat: - raw.exportFormat === "gif" ? (raw.exportFormat as ExportFormat) : DEFAULT_PREFS.exportFormat, + raw.exportFormat === "gif" || raw.exportFormat === "mp4" + ? (raw.exportFormat as ExportFormat) + : DEFAULT_PREFS.exportFormat, }; } From a8427b950e29641a38b136cd2afae7f96e5dc388 Mon Sep 17 00:00:00 2001 From: JasonOA888 Date: Mon, 6 Apr 2026 02:01:01 +0800 Subject: [PATCH 4/4] fix: resolve lint errors for CI - Add updateState to useEffect dependency array - Remove ineffective biome-ignore suppression comment - Fix formatting in userPreferences.ts per biome rules --- src/components/video-editor/VideoEditor.tsx | 6 ++---- src/lib/userPreferences.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 4168ef8c..a85ccafc 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -364,7 +364,7 @@ export default function VideoEditor() { // overwriting saved prefs with defaults on the first render const [prefsHydrated, setPrefsHydrated] = useState(false); - // Load persisted user preferences on mount + // Load persisted user preferences on mount (intentionally runs once) useEffect(() => { const prefs = loadUserPreferences(); updateState({ @@ -374,9 +374,7 @@ export default function VideoEditor() { setExportQuality(prefs.exportQuality); setExportFormat(prefs.exportFormat); setPrefsHydrated(true); - // We intentionally only want this to run once on mount - // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect - }, []); + }, [updateState]); // Auto-save user preferences when settings change useEffect(() => { diff --git a/src/lib/userPreferences.ts b/src/lib/userPreferences.ts index 58397996..e0607880 100644 --- a/src/lib/userPreferences.ts +++ b/src/lib/userPreferences.ts @@ -56,7 +56,10 @@ export function loadUserPreferences(): UserPreferences { return { padding: - typeof raw.padding === "number" && Number.isFinite(raw.padding) && raw.padding >= 0 && raw.padding <= 100 + typeof raw.padding === "number" && + Number.isFinite(raw.padding) && + raw.padding >= 0 && + raw.padding <= 100 ? raw.padding : DEFAULT_PREFS.padding, aspectRatio: @@ -64,7 +67,9 @@ export function loadUserPreferences(): UserPreferences { ? (raw.aspectRatio as AspectRatio) : DEFAULT_PREFS.aspectRatio, exportQuality: - raw.exportQuality === "medium" || raw.exportQuality === "good" || raw.exportQuality === "source" + raw.exportQuality === "medium" || + raw.exportQuality === "good" || + raw.exportQuality === "source" ? (raw.exportQuality as ExportQuality) : DEFAULT_PREFS.exportQuality, exportFormat: