Skip to content

feat: custom resolution picker#429

Open
harryfrzz wants to merge 2 commits intosiddharthvaddem:mainfrom
harryfrzz:feat/custom-resolution
Open

feat: custom resolution picker#429
harryfrzz wants to merge 2 commits intosiddharthvaddem:mainfrom
harryfrzz:feat/custom-resolution

Conversation

@harryfrzz
Copy link
Copy Markdown

@harryfrzz harryfrzz commented Apr 12, 2026

Description

Added a custom resolution selector modal that allows users to specify exact width and height for video exports, giving more flexibility beyond the preset quality options (low, medium, high, source).

Motivation

Users need fine-grained control over export resolution. The existing preset options don't accommodate specific use cases like creating videos for particular display sizes or social media platforms with specific dimension requirements.

Type of Change

  • New Feature

Changes Made

  • Created CustomExportModal.tsx component for resolution input
  • Added customResolution and customExportModalOpen props to SettingsPanel
  • Added custom resolution type and related exports in lib/exporter
  • Integrated modal with export quality flow
  • Added i18n translations for custom export strings

Screenshots

Screenshot 2026-04-12 at 1 46 17 PM Screenshot 2026-04-12 at 1 46 08 PM

Related Issue(s)

#227

Testing

  1. Open the video editor settings panel
  2. In the export quality section, click the "Custom" button (gear icon)
  3. Enter custom width/height values and click Apply
  4. Verify the resolution is reflected in export settings
  5. Clicking Custom button again should re-open the modal

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

Thank you for contributing!

Summary by CodeRabbit

  • New Features
    • Added custom export quality option for video exports. Users can now specify custom resolution dimensions by entering precise width and height values. The feature includes built-in validation to enforce minimum dimension requirements and automatic formatting to ensure optimal video quality during export.

- Add missing props for custom resolution and export modal
- Fix export quality callback type mismatch
- Add CustomExportModal component integration
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces custom resolution export capability to the video editor. It adds a new CustomExportModal component for user input, extends SettingsPanel with a "custom" quality button, updates VideoEditor to manage custom resolution state, and adds corresponding type definitions and translations.

Changes

Cohort / File(s) Summary
Type Definitions
src/lib/exporter/types.ts, src/lib/exporter/index.ts
Expanded ExportQuality type to include "custom" option and introduced new CustomResolution interface with width/height fields. Re-exported from main exporter module.
New Modal Component
src/components/video-editor/CustomExportModal.tsx
New React component providing a dialog for custom resolution input. Manages temporary width/height state, applies dimension validation (min 100px, even values), and triggers callbacks to update parent state and close modal.
Settings Panel Integration
src/components/video-editor/SettingsPanel.tsx
Extended to support custom quality option with settings icon, changes quality selector layout from 3 to 4 columns, displays resolution readout when custom quality is active, and conditionally renders CustomExportModal with wired callbacks.
Editor State Management
src/components/video-editor/VideoEditor.tsx
Added customResolution state and showCustomExportModal boolean. Extended MP4 export logic to derive width/height/bitrate from custom resolution when quality is "custom", and passed new props to SettingsPanel.
Localization
src/i18n/locales/en/settings.json
Added "custom": "Custom" entry to exportQuality translation object.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • siddharthvaddem

Poem

🎬 dimensions dance in custom frames,
resolution now plays by your names—
width and height, no longer confined,
export magic, perfectly aligned ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the main feature being added—a custom resolution picker for video exports.
Description check ✅ Passed PR description covers all required template sections: motivation, type of change, screenshots, related issues, testing steps, and completed checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/video-editor/VideoEditor.tsx (1)

1573-1596: ⚠️ Potential issue | 🔴 Critical

Missing customResolution in dependency array

lowkey risky - customResolution is used in handleExport (line 1463) but isn't in the dependency array. if user changes the custom resolution and then exports, they might get stale values due to closure capture.

🐛 Proposed fix
 	[
 		videoPath,
 		webcamVideoPath,
 		wallpaper,
 		zoomRegions,
 		trimRegions,
 		speedRegions,
 		shadowIntensity,
 		showBlur,
 		motionBlurAmount,
 		borderRadius,
 		padding,
 		cropRegion,
 		annotationRegions,
 		isPlaying,
 		aspectRatio,
 		webcamLayoutPreset,
 		webcamMaskShape,
 		webcamSizePreset,
 		webcamPosition,
 		exportQuality,
+		customResolution,
 		handleExportSaved,
 		cursorTelemetry,
 	],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1573 - 1596, The
dependency array for the effect/callback that references handleExport (and
ultimately reads customResolution) is missing customResolution, which can cause
stale closure values; update the dependency array (the array listing videoPath,
webcamVideoPath, ..., cursorTelemetry) to also include customResolution so that
handleExport and any effect re-evaluate when customResolution changes, ensuring
the exported video uses the latest resolution value.
🧹 Nitpick comments (3)
src/components/video-editor/CustomExportModal.tsx (2)

33-41: Silent failure when validation fails

if width or height is < 100, handleApply just... does nothing. user has no idea why clicking Apply didn't work. might want a toast or inline error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/CustomExportModal.tsx` around lines 33 - 41,
handleApply silently returns when computed width or height is < 100; add user
feedback so the user knows why Apply failed. Update the CustomExportModal
component to validate the computed values in handleApply (using
tempCustomWidth/tempCustomHeight) and when invalid set an error state (e.g.,
customResolutionError) or call the existing toast/notification utility, then
show that message inline in the modal or as a toast instead of doing nothing;
keep the existing success flow that calls onCustomResolutionChange,
onExportQualityChange("custom"), and onOpenChange(false) when values are valid.

59-69: Hardcoded English labels

nit: "Width (px)" and "Height (px)" aren't using t() for i18n. not a blocker but inconsistent with the rest of the modal which does use t("exportQuality.custom").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/CustomExportModal.tsx` around lines 59 - 69,
Replace the hardcoded label strings "Width (px)" and "Height (px)" in the
CustomExportModal component with i18n calls using the existing translation
function (t). Locate the label elements near the inputs that use tempCustomWidth
/ setTempCustomWidth and tempCustomHeight / setTempCustomHeight and change their
text to t("export.width") and t("export.height") (or equivalent keys used in
your locale files), ensuring the same translation namespace/keys style as
t("exportQuality.custom") used elsewhere in the modal and update locale files
with those keys if missing.
src/components/video-editor/SettingsPanel.tsx (1)

1574-1576: nit: cleaner type handling

the cast here is a bit verbose. since onExportQualityChange already accepts ExportQuality, you could just do:

onExportQualityChange={() => onExportQualityChange?.("custom")}

or if you need to keep the callback signature for the child:

onExportQualityChange={onExportQualityChange as (quality: "custom") => void}

not a big deal, just reads kinda cursed at first glance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 1574 - 1576, The
current prop assignment for onExportQualityChange uses an awkward inline cast
that reduces readability; replace the verbose wrapper with a simpler form by
either invoking the parent callback with the literal value (call
onExportQualityChange?.("custom") inside a zero-arg arrow) or, if preserving the
child callback signature is required, cast the prop directly
(onExportQualityChange as (quality: "custom") => void); update the occurrence of
onExportQualityChange in SettingsPanel.tsx so it delegates cleanly to the
existing onExportQualityChange: ExportQuality handler instead of using the
complex inline type assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/CustomExportModal.tsx`:
- Around line 30-31: The tempCustomWidth/tempCustomHeight local state is only
initialized once so it becomes stale when the prop customResolution changes; add
a useEffect that watches customResolution and calls
setTempCustomWidth(customResolution?.width ?? 1920) and
setTempCustomHeight(customResolution?.height ?? 1080) to sync the inputs
whenever the prop changes (preserve the current defaults if customResolution is
undefined).

In `@src/i18n/locales/en/settings.json`:
- Around line 79-80: The English locale added a new "custom" key under
exportQuality but other locale files (e.g., Spanish, French, Chinese, Korean,
Turkish) are missing it; update every translation file that defines
exportQuality to include the "custom" key with the appropriate translated string
(or a temporary translated placeholder) so all locales mirror the English keys
and prevent fallback text — locate the exportQuality object in each locale JSON
and add the "custom" entry alongside "high" (and any other quality keys).

In `@src/lib/exporter/types.ts`:
- Line 30: The exportQuality validation logic must be updated to accept the new
"custom" variant: update the validation in userPreferences.ts (the function that
checks/sanitizes the exportQuality field, e.g., validateUserPreferences or
similar) so it recognizes "medium" | "good" | "source" | "custom" (matching the
ExportQuality type) instead of only the prior set, and likewise update the
project persistence check in projectPersistence.ts (the routine that sanitizes
project export preferences, e.g., sanitizeProjectPreferences or the
exportQuality validation block) to allow "custom" — ensure both places validate
against all ExportQuality options and only fall back to defaults for truly
unknown values.

---

Outside diff comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1573-1596: The dependency array for the effect/callback that
references handleExport (and ultimately reads customResolution) is missing
customResolution, which can cause stale closure values; update the dependency
array (the array listing videoPath, webcamVideoPath, ..., cursorTelemetry) to
also include customResolution so that handleExport and any effect re-evaluate
when customResolution changes, ensuring the exported video uses the latest
resolution value.

---

Nitpick comments:
In `@src/components/video-editor/CustomExportModal.tsx`:
- Around line 33-41: handleApply silently returns when computed width or height
is < 100; add user feedback so the user knows why Apply failed. Update the
CustomExportModal component to validate the computed values in handleApply
(using tempCustomWidth/tempCustomHeight) and when invalid set an error state
(e.g., customResolutionError) or call the existing toast/notification utility,
then show that message inline in the modal or as a toast instead of doing
nothing; keep the existing success flow that calls onCustomResolutionChange,
onExportQualityChange("custom"), and onOpenChange(false) when values are valid.
- Around line 59-69: Replace the hardcoded label strings "Width (px)" and
"Height (px)" in the CustomExportModal component with i18n calls using the
existing translation function (t). Locate the label elements near the inputs
that use tempCustomWidth / setTempCustomWidth and tempCustomHeight /
setTempCustomHeight and change their text to t("export.width") and
t("export.height") (or equivalent keys used in your locale files), ensuring the
same translation namespace/keys style as t("exportQuality.custom") used
elsewhere in the modal and update locale files with those keys if missing.

In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 1574-1576: The current prop assignment for onExportQualityChange
uses an awkward inline cast that reduces readability; replace the verbose
wrapper with a simpler form by either invoking the parent callback with the
literal value (call onExportQualityChange?.("custom") inside a zero-arg arrow)
or, if preserving the child callback signature is required, cast the prop
directly (onExportQualityChange as (quality: "custom") => void); update the
occurrence of onExportQualityChange in SettingsPanel.tsx so it delegates cleanly
to the existing onExportQualityChange: ExportQuality handler instead of using
the complex inline type assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5258c09-884d-4196-ab61-4d1668984f85

📥 Commits

Reviewing files that changed from the base of the PR and between a6ae0e6 and 09114a4.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • src/components/video-editor/CustomExportModal.tsx
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/i18n/locales/en/settings.json
  • src/lib/exporter/index.ts
  • src/lib/exporter/types.ts

Comment on lines +30 to +31
const [tempCustomWidth, setTempCustomWidth] = useState(customResolution?.width || 1920);
const [tempCustomHeight, setTempCustomHeight] = useState(customResolution?.height || 1080);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stale state when modal reopens with different customResolution

the useState initializers only run on mount. if user opens modal, sets 1280x720, closes, then you change customResolution externally and reopen - the inputs still show 1280x720 instead of the new value.

you probably want a useEffect to sync when the prop changes:

🔧 Proposed fix
+import { useState, useEffect } from "react";

 const [tempCustomWidth, setTempCustomWidth] = useState(customResolution?.width || 1920);
 const [tempCustomHeight, setTempCustomHeight] = useState(customResolution?.height || 1080);

+useEffect(() => {
+	if (open) {
+		setTempCustomWidth(customResolution?.width || 1920);
+		setTempCustomHeight(customResolution?.height || 1080);
+	}
+}, [open, customResolution]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [tempCustomWidth, setTempCustomWidth] = useState(customResolution?.width || 1920);
const [tempCustomHeight, setTempCustomHeight] = useState(customResolution?.height || 1080);
import { useState, useEffect } from "react";
const [tempCustomWidth, setTempCustomWidth] = useState(customResolution?.width || 1920);
const [tempCustomHeight, setTempCustomHeight] = useState(customResolution?.height || 1080);
useEffect(() => {
if (open) {
setTempCustomWidth(customResolution?.width || 1920);
setTempCustomHeight(customResolution?.height || 1080);
}
}, [open, customResolution]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/CustomExportModal.tsx` around lines 30 - 31, The
tempCustomWidth/tempCustomHeight local state is only initialized once so it
becomes stale when the prop customResolution changes; add a useEffect that
watches customResolution and calls setTempCustomWidth(customResolution?.width ??
1920) and setTempCustomHeight(customResolution?.height ?? 1080) to sync the
inputs whenever the prop changes (preserve the current defaults if
customResolution is undefined).

Comment on lines +79 to +80
"high": "High",
"custom": "Custom"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check all locale files for exportQuality.custom key
echo "=== Checking all locale files for 'custom' key in exportQuality ==="
fd -t f "settings.json" src/i18n/locales --exec grep -l "exportQuality" {} \; | while read f; do
  echo "--- $f ---"
  grep -A6 '"exportQuality"' "$f"
done

Repository: siddharthvaddem/openscreen

Length of output: 1145


Missing translation across all locales

the english locale got the new "custom" key in exportQuality, but it's missing from literally every other locale - spanish, french, chinese, korean, turkish all need it. this is gonna cause fallback text for like 90% of your users, lowkey risky for a export feature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/en/settings.json` around lines 79 - 80, The English locale
added a new "custom" key under exportQuality but other locale files (e.g.,
Spanish, French, Chinese, Korean, Turkish) are missing it; update every
translation file that defines exportQuality to include the "custom" key with the
appropriate translated string (or a temporary translated placeholder) so all
locales mirror the English keys and prevent fallback text — locate the
exportQuality object in each locale JSON and add the "custom" entry alongside
"high" (and any other quality keys).

Comment thread src/lib/exporter/types.ts
}

export type ExportQuality = "medium" | "good" | "source";
export type ExportQuality = "medium" | "good" | "source" | "custom";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the userPreferences.ts validation logic
rg -n -A5 "exportQuality:" src/lib/userPreferences.ts

Repository: siddharthvaddem/openscreen

Length of output: 617


🏁 Script executed:

# First, verify "custom" is actually in the ExportQuality type
cat -n src/lib/exporter/types.ts | head -40

Repository: siddharthvaddem/openscreen

Length of output: 1217


🏁 Script executed:

# Check if there are other places validating or checking exportQuality values
rg -n "exportQuality.*==|exportQuality.*===|ExportQuality" src/ --type ts

Repository: siddharthvaddem/openscreen

Length of output: 3098


🏁 Script executed:

# Check the context of the validation in projectPersistence.ts
sed -n '450,460p' src/components/video-editor/projectPersistence.ts

Repository: siddharthvaddem/openscreen

Length of output: 482


Adding "custom" breaks user preference validation in two places

heads up - both userPreferences.ts (lines 70-72) and projectPersistence.ts (lines 454-456) have incomplete validation for exportQuality. when a user picks custom quality, saves preferences/project, and reloads, it'll silently revert to defaults. lowkey risky for user experience.

need to update userPreferences.ts validation:

userPreferences.ts fix
 exportQuality:
 	raw.exportQuality === "medium" ||
 	raw.exportQuality === "good" ||
-	raw.exportQuality === "source"
+	raw.exportQuality === "source" ||
+	raw.exportQuality === "custom"
 		? (raw.exportQuality as ExportQuality)
 		: DEFAULT_PREFS.exportQuality,

also fix projectPersistence.ts - the current check only validates "medium" || "source" and silently reverts "custom" to "good". update it to handle all quality options.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/types.ts` at line 30, The exportQuality validation logic
must be updated to accept the new "custom" variant: update the validation in
userPreferences.ts (the function that checks/sanitizes the exportQuality field,
e.g., validateUserPreferences or similar) so it recognizes "medium" | "good" |
"source" | "custom" (matching the ExportQuality type) instead of only the prior
set, and likewise update the project persistence check in projectPersistence.ts
(the routine that sanitizes project export preferences, e.g.,
sanitizeProjectPreferences or the exportQuality validation block) to allow
"custom" — ensure both places validate against all ExportQuality options and
only fall back to defaults for truly unknown values.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09114a48d0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/lib/exporter/types.ts
}

export type ExportQuality = "medium" | "good" | "source";
export type ExportQuality = "medium" | "good" | "source" | "custom";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist custom quality through project and preference loaders

After adding "custom" to ExportQuality, the deserializers still reject it: normalizeProjectEditor in src/components/video-editor/projectPersistence.ts (lines 453-456) coerces anything other than "medium"/"source" to "good", and loadUserPreferences in src/lib/userPreferences.ts (lines 69-74) only accepts "medium"|"good"|"source". This means users who choose Custom will lose that mode (and thus their custom dimensions) after reopening a project or restarting the app, causing exports to silently revert to preset resolutions.

Useful? React with 👍 / 👎.

Comment on lines +1463 to +1466
} else if (quality === "custom" && customResolution) {
// Use custom resolution
exportWidth = Math.floor(customResolution.width / 2) * 2;
exportHeight = Math.floor(customResolution.height / 2) * 2;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include custom resolution in export callback dependencies

handleExport now reads customResolution for quality === "custom", but customResolution is not in that useCallback dependency list. If the user edits custom width/height while quality stays "custom", the callback can keep the previous captured resolution and export with stale dimensions. Adding customResolution to the dependencies (or reading from a ref) is needed so each Apply uses the latest values.

Useful? React with 👍 / 👎.

@harryfrzz
Copy link
Copy Markdown
Author

@siddharthvaddem Please take a look at this feature

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant