Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 151 additions & 22 deletions src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Palette, Trash as Trash2, UploadSimple as Upload, X } from "@phosphor-icons/react";
import { Link, LinkBreak, Palette, Trash as Trash2, UploadSimple as Upload, X } from "@phosphor-icons/react";
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
Expand Down Expand Up @@ -43,13 +43,17 @@ import type {
CursorStyle,
EditorEffectSection,
FigureData,
Padding,
PlaybackSpeed,
WebcamOverlaySettings,
WebcamPositionPreset,
ZoomDepth,
ZoomMode,
ZoomTransitionEasing,
} from "./types";
import {
isZeroPadding,
} from "./videoPlayback/layoutUtils";
import {
DEFAULT_AUTO_CAPTION_SETTINGS,
DEFAULT_CROP_REGION,
Expand All @@ -60,6 +64,7 @@ import {
DEFAULT_CURSOR_SMOOTHING,
DEFAULT_CURSOR_STYLE,
DEFAULT_CURSOR_SWAY,
DEFAULT_PADDING,
DEFAULT_WEBCAM_CORNER_RADIUS,
DEFAULT_WEBCAM_MARGIN,
DEFAULT_WEBCAM_POSITION_PRESET,
Expand Down Expand Up @@ -394,8 +399,8 @@ interface SettingsPanelProps {
onWebcamChange?: (webcam: WebcamOverlaySettings) => void;
onUploadWebcam?: () => void;
onClearWebcam?: () => void;
padding?: number;
onPaddingChange?: (padding: number) => void;
padding?: Padding;
onPaddingChange?: (padding: Padding) => void;
frame?: string | null;
onFrameChange?: (frameId: string | null) => void;
cropRegion?: CropRegion;
Expand Down Expand Up @@ -733,7 +738,7 @@ export function SettingsPanel({
onWebcamChange,
onUploadWebcam,
onClearWebcam,
padding = 50,
padding = DEFAULT_PADDING,
onPaddingChange,
frame = null,
onFrameChange,
Expand Down Expand Up @@ -786,7 +791,7 @@ export function SettingsPanel({
);
const removeBackgroundStateRef = useRef<{
aspectRatio: AspectRatio;
padding: number;
padding: Padding;
} | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const builtInWallpaperPaths = useMemo(
Expand Down Expand Up @@ -914,7 +919,7 @@ export function SettingsPanel({
const [gradient, setGradient] = useState<string>(
GRADIENTS.includes(selected) ? selected : GRADIENTS[0],
);
const removeBackgroundEnabled = aspectRatio === "native" && padding === 0;
const removeBackgroundEnabled = aspectRatio === "native" && isZeroPadding(padding);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Device frames from extension system
const [availableFrames, setAvailableFrames] = useState<FrameInstance[]>([]);
Expand Down Expand Up @@ -1125,14 +1130,60 @@ export function SettingsPanel({
padding,
};
onAspectRatioChange?.("native");
onPaddingChange?.(0);
onPaddingChange?.({ top: 0, bottom: 0, left: 0, right: 0, linked: padding.linked });
return;
}

if (removeBackgroundStateRef.current) {
onAspectRatioChange?.(removeBackgroundStateRef.current.aspectRatio);
onPaddingChange?.(removeBackgroundStateRef.current.padding);
const previousState = removeBackgroundStateRef.current;
if (previousState) {
onAspectRatioChange?.(previousState.aspectRatio);
onPaddingChange?.(previousState.padding);
removeBackgroundStateRef.current = null;
return;
}

// Fallback if the project loaded in a "background removed" state already
onAspectRatioChange?.(initialEditorPreferences.aspectRatio);
onPaddingChange?.({ ...DEFAULT_PADDING });
};

const togglePaddingLink = () => {
const isLinked = padding.linked !== false;
const nextLinked = !isLinked;
if (nextLinked) {
// Compute average for relinking to avoid sudden shifts
const avg = Math.round(
(padding.top + padding.bottom + padding.left + padding.right) / 4,
);
onPaddingChange?.({
top: avg,
bottom: avg,
left: avg,
right: avg,
linked: true,
});
} else {
onPaddingChange?.({
...padding,
linked: false,
});
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const handlePaddingSideChange = (side: keyof Padding, value: number) => {
if (padding.linked !== false) {
onPaddingChange?.({
top: value,
bottom: value,
left: value,
right: value,
linked: true,
});
} else {
onPaddingChange?.({
...padding,
[side]: value,
});
}
};

Expand Down Expand Up @@ -1288,7 +1339,7 @@ export function SettingsPanel({
const resetFrameSection = () => {
onShadowChange?.(initialEditorPreferences.shadowIntensity);
onBorderRadiusChange?.(initialEditorPreferences.borderRadius);
onPaddingChange?.(initialEditorPreferences.padding);
onPaddingChange?.(DEFAULT_PADDING);
onFrameChange?.(null);
onAspectRatioChange?.(initialEditorPreferences.aspectRatio);
removeBackgroundStateRef.current = null;
Expand Down Expand Up @@ -1778,17 +1829,95 @@ export function SettingsPanel({
formatValue={(v) => `${v}px`}
parseInput={(text) => parseFloat(text.replace(/px$/, ""))}
/>
<SliderControl
label={tSettings("effects.padding")}
value={padding}
defaultValue={initialEditorPreferences.padding}
min={0}
max={100}
step={1}
onChange={(v) => onPaddingChange?.(v)}
formatValue={(v) => `${v}%`}
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
/>
<div className="flex flex-col gap-1.5 pt-0.5">
<div className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">
{tSettings("effects.padding")}
</span>
<button
type="button"
onClick={togglePaddingLink}
className={cn(
"p-1 rounded-md transition-colors",
padding.linked !== false
? "text-[#2563EB] bg-[#2563EB]/10"
: "text-muted-foreground hover:bg-foreground/[0.05]",
)}
title={
padding.linked !== false
? tSettings("effects.paddingLinked", "Linked (Uniform)")
: tSettings("effects.paddingUnlinked", "Unlinked (Asymmetrical)")
}
>
{padding.linked !== false ? (
<Link size={12} weight="bold" />
) : (
<LinkBreak size={12} weight="bold" />
)}
</button>
</div>

{padding.linked !== false ? (
<SliderControl
label=""
value={padding.top}
defaultValue={DEFAULT_PADDING.top}
min={0}
max={100}
step={1}
onChange={(v) => handlePaddingSideChange("top", v)}
formatValue={(v) => `${v}%`}
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
) : (
<div className="grid grid-cols-2 gap-x-3 gap-y-1.5">
<SliderControl
label={tSettings("effects.paddingTop", "Top")}
value={padding.top}
defaultValue={DEFAULT_PADDING.top}
min={0}
max={100}
step={1}
onChange={(v) => handlePaddingSideChange("top", v)}
formatValue={(v) => `${v}%`}
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
/>
<SliderControl
label={tSettings("effects.paddingBottom", "Bottom")}
value={padding.bottom}
defaultValue={DEFAULT_PADDING.bottom}
min={0}
max={100}
step={1}
onChange={(v) => handlePaddingSideChange("bottom", v)}
formatValue={(v) => `${v}%`}
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
/>
<SliderControl
label={tSettings("effects.paddingLeft", "Left")}
value={padding.left}
defaultValue={DEFAULT_PADDING.left}
min={0}
max={100}
step={1}
onChange={(v) => handlePaddingSideChange("left", v)}
formatValue={(v) => `${v}%`}
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
/>
<SliderControl
label={tSettings("effects.paddingRight", "Right")}
value={padding.right}
defaultValue={DEFAULT_PADDING.right}
min={0}
max={100}
step={1}
onChange={(v) => handlePaddingSideChange("right", v)}
formatValue={(v) => `${v}%`}
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
/>
</div>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
Comment on lines +1832 to +1920

Copy link
Copy Markdown
Contributor

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
# Find all settings locale files and show the "effects.padding" entry + surrounding keys
fd -t f 'settings.json' | xargs -I {} sh -c 'echo "=== {} ==="; rg -nP -C2 "\"padding\"" {} || true'
# Also check what tSettings resolution looks like for effects.padding vs effects.padding.top
rg -nP -C2 '"padding"\s*:' --type=json

Repository: webadderall/Recordly

Length of output: 2399


🏁 Script executed:

#!/bin/bash
# Check for the nested padding keys that the code references
rg '"effects\.padding\.(linked|unlinked|top|bottom|left|right)"' src/i18n/locales/ || echo "No nested padding keys found"

# Also show the full effects section structure from one locale to understand the hierarchy
echo "=== Full effects section in en/settings.json ==="
rg -A 20 '"effects":\s*{' src/i18n/locales/en/settings.json | head -30

Repository: webadderall/Recordly

Length of output: 250


🏁 Script executed:

#!/bin/bash
# Show the full effects section from en locale
rg -A 50 '"effects":' src/i18n/locales/en/settings.json | head -60

Repository: webadderall/Recordly

Length of output: 1984


🏁 Script executed:

#!/bin/bash
# Search for padding section in effects
rg -A 2 -B 2 '"padding"' src/i18n/locales/en/settings.json

# Also check the full JSON structure to understand nesting
echo "=== Checking if effects.padding is an object or string ==="
python3 <<'EOF'
import json
with open('src/i18n/locales/en/settings.json', 'r') as f:
    data = json.load(f)
    if 'effects' in data and 'padding' in data['effects']:
        padding_val = data['effects']['padding']
        print(f"Type of effects.padding: {type(padding_val).__name__}")
        print(f"Value: {padding_val}")
        if isinstance(padding_val, dict):
            print(f"Keys: {list(padding_val.keys())}")
EOF

Repository: webadderall/Recordly

Length of output: 286


i18n key collision: effects.padding is a string value, not an object—nested keys cannot coexist.

Line 1835 calls tSettings("effects.padding") to fetch the "Padding" label (which exists as a string in all locale files). However, lines 1848, 1849, 1875, 1886, 1897, and 1908 call tSettings("effects.padding.linked" | ".unlinked" | ".top" | ".bottom" | ".left" | ".right", ...). In JSON i18n structures, a key cannot simultaneously be a string value and a parent object—the nested keys do not exist in any locale file, so all new padding UI text (link toggle labels and side labels) will always render in English, regardless of the user's language setting.

Add the corresponding nested keys to each locale file, or use a disjoint namespace (e.g., padding.top instead of effects.padding.top, mirroring the pattern used for crops):

-							title={
-								padding.linked !== false
-									? tSettings("effects.padding.linked", "Linked (Uniform)")
-									: tSettings("effects.padding.unlinked", "Unlinked (Asymmetrical)")
-							}
+							title={
+								padding.linked !== false
+									? tSettings("padding.linked", "Linked (Uniform)")
+									: tSettings("padding.unlinked", "Unlinked (Asymmetrical)")
+							}

Then add "padding": { "linked": "...", "unlinked": "...", "top": "...", ... } to each locale file, or migrate all refs to the padding.* namespace.

🤖 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 1832 - 1920, The
i18n collision is that tSettings("effects.padding") is a string but the
component also calls nested keys like "effects.padding.linked",
"effects.padding.unlinked", "effects.padding.top" etc., which don't exist; fix
by either (A) adding a nested "padding" object under the same locale path (e.g.,
in each locale JSON add effects.padding: { "label": "...", "linked":"...",
"unlinked":"...", "top":"...", "bottom":"...", "left":"...", "right":"..." } and
change the component to use tSettings("effects.padding.label") for the main
label) or (B) change the component keys to a separate namespace (e.g., replace
tSettings("effects.padding") with tSettings("effects.paddingLabel") and all
nested calls to tSettings("padding.top") / "padding.linked" etc.), ensuring all
locale files are updated to include the chosen keys so tSettings calls (seen in
this file) resolve for all languages.

<div className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-1.5">
<span className="text-[10px] text-muted-foreground">
{tSettings("effects.removeBackground")}
Expand Down
7 changes: 4 additions & 3 deletions src/components/video-editor/VideoPlayback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
type AutoCaptionSettings,
type CaptionCue,
type CursorStyle,
type CursorTelemetryPoint,
type Padding,
type SpeedRegion,
type TrimRegion,
type WebcamOverlaySettings,
Expand Down Expand Up @@ -110,6 +110,7 @@ import {
DEFAULT_ZOOM_IN_OVERLAP_MS,
DEFAULT_ZOOM_OUT_DURATION_MS,
DEFAULT_ZOOM_OUT_EASING,
DEFAULT_PADDING,
getDefaultCaptionFontFamily,
} from "./types";
import {
Expand Down Expand Up @@ -241,7 +242,7 @@ interface VideoPlaybackProps {
zoomOutEasing?: ZoomTransitionEasing;
connectedZoomEasing?: ZoomTransitionEasing;
borderRadius?: number;
padding?: number;
padding?: Padding | number;
frame?: string | null;
cropRegion?: import("./types").CropRegion;
webcam?: WebcamOverlaySettings;
Expand Down Expand Up @@ -311,7 +312,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
zoomOutEasing = DEFAULT_ZOOM_OUT_EASING,
connectedZoomEasing = DEFAULT_CONNECTED_ZOOM_EASING,
borderRadius = 0,
padding = 50,
padding = DEFAULT_PADDING,
frame = null,
cropRegion,
webcam,
Expand Down
29 changes: 27 additions & 2 deletions src/components/video-editor/projectPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ import {
DEFAULT_ZOOM_OUT_DURATION_MS,
DEFAULT_ZOOM_OUT_EASING,
getDefaultCaptionFontFamily,
type Padding,
type SpeedRegion,
type TrimRegion,
type WebcamOverlaySettings,
type ZoomRegion,
type ZoomTransitionEasing,
DEFAULT_PADDING,
} from "./types";

export const PROJECT_VERSION = 1;
Expand Down Expand Up @@ -91,7 +93,7 @@ export interface ProjectEditorState {
cursorClickBounceDuration: number;
cursorSway: number;
borderRadius: number;
padding: number;
padding: Padding;
/** Selected frame ID (e.g. "recordly.frames/browser-dark"), or null for none */
frame: string | null;
cropRegion: CropRegion;
Expand Down Expand Up @@ -748,7 +750,30 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
? clamp((editor as Partial<ProjectEditorState>).cursorSway as number, 0, 2)
: DEFAULT_CURSOR_SWAY,
borderRadius: typeof editor.borderRadius === "number" ? editor.borderRadius : 12.5,
padding: isFiniteNumber(editor.padding) ? clamp(editor.padding, 0, 100) : 20,
padding: (() => {
const p = editor.padding;
if (p && typeof p === "object") {
const linked = typeof p.linked === "boolean" ? p.linked : true;
const top = isFiniteNumber(p.top) ? clamp(p.top, 0, 100) : DEFAULT_PADDING.top;
if (linked) {
return { top, bottom: top, left: top, right: top, linked: true };
}
return {
top,
bottom: isFiniteNumber(p.bottom)
? clamp(p.bottom, 0, 100)
: DEFAULT_PADDING.bottom,
left: isFiniteNumber(p.left) ? clamp(p.left, 0, 100) : DEFAULT_PADDING.left,
right: isFiniteNumber(p.right) ? clamp(p.right, 0, 100) : DEFAULT_PADDING.right,
linked: false,
};
}
if (typeof p === "number" && isFiniteNumber(p)) {
const val = clamp(p, 0, 100);
return { top: val, bottom: val, left: val, right: val, linked: true };
}
return { ...DEFAULT_PADDING };
})(),
frame: typeof editor.frame === "string" ? editor.frame : null,
cropRegion: {
x: cropX,
Expand Down
16 changes: 16 additions & 0 deletions src/components/video-editor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,22 @@ export const DEFAULT_CROP_REGION: CropRegion = {
height: 1,
};

export interface Padding {
top: number;
bottom: number;
left: number;
right: number;
linked?: boolean;
}

export const DEFAULT_PADDING: Padding = {
top: 50,
bottom: 50,
left: 50,
right: 50,
linked: true,
};

export interface AudioRegion {
id: string;
startMs: number;
Expand Down
Loading