diff --git a/README.md b/README.md
index 53d5479..b42355e 100644
--- a/README.md
+++ b/README.md
@@ -25,21 +25,20 @@ Screen Studio is an awesome product and this is definitely not a 1:1 clone. Open
OpenScreen is 100% free for personal and commercial use. Use it, modify it, distribute it. (Just be cool 😁 and give a shoutout if you feel like it !)
-
-
+
+
## Core Features
-- Record your whole screen or specific windows.
-- Add Automatic zooms or manual zooms (customizable depth levels).
-- Record microphone audio and system audio capture.
-- Customize the duration and position of zooms however you please.
+- Record specific windows or your whole screen.
+- Add automatic or manual zooms (adjustable depth levels) and customize their durarion and position.
+- Record microphone and system audio.
- Crop video recordings to hide parts.
- Choose between wallpapers, solid colors, gradients or a custom background.
- Motion blur for smoother pan and zoom effects.
- Add annotations (text, arrows, images).
- Trim sections of the clip.
-- Customize speed at different segments.
+- Customize the speed of different segments.
- Export in different aspect ratios and resolutions.
## Installation
@@ -78,9 +77,9 @@ You may need to grant screen recording permissions depending on your desktop env
System audio capture relies on Electron's [desktopCapturer](https://www.electronjs.org/docs/latest/api/desktop-capturer) and has some platform-specific quirks:
-- **macOS**: Requires macOS 13+. On macOS 14.2+ you'll be prompted to grant audio capture permission. macOS 12 and below does not support system audio (mic still work).
+- **macOS**: Requires macOS 13+. On macOS 14.2+ you'll be prompted to grant audio capture permission. macOS 12 and below does not support system audio (mic still works).
- **Windows**: Works out of the box.
-- **Linux**: Needs PipeWire (default on Ubuntu 22.04+, Fedora 34+). Older PulseAudio-only setups may not support system audio (mic should still works).
+- **Linux**: Needs PipeWire (default on Ubuntu 22.04+, Fedora 34+). Older PulseAudio-only setups may not support system audio (mic should still work).
## Built with
- Electron
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index e33cb0b..a27fbb9 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -23,6 +23,7 @@ import {
import { computeFrameStepTime } from "@/lib/frameStep";
import type { ProjectMedia } from "@/lib/recordingSession";
import { matchesShortcut } from "@/lib/shortcuts";
+import { loadUserPreferences, saveUserPreferences } from "@/lib/userPreferences";
import {
getAspectRatioValue,
getNativeAspectRatioValue,
@@ -366,6 +367,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 0000000..e060788
--- /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)
+ }
+}