Skip to content
Open
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
31 changes: 21 additions & 10 deletions src/hooks/useVideoEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,6 @@ export function useVideoEditor() {
return track;
}, [addTrack]);

const updateRecipe = useCallback((patch: Partial<EditRecipe>) => {
setRecipe((prev) => {
const next = { ...prev, ...patch };
// GIF has no audio — force keepAudio off
if (next.format === "gif") {
next.keepAudio = false;
}
return next;
});
}, []);
const isValidValue = (key: keyof EditRecipe, val: any): boolean => {
switch (key) {
case "preset":
Expand Down Expand Up @@ -254,6 +244,23 @@ export function useVideoEditor() {
}
};

const updateRecipe = useCallback((patch: Partial<EditRecipe>) => {
setRecipe((prev) => {
const validated: Partial<EditRecipe> = {};
for (const [key, val] of Object.entries(patch)) {
if (isValidValue(key as keyof EditRecipe, val)) {
(validated as any)[key] = val;
}
}
const next = { ...prev, ...validated };
// GIF has no audio — force keepAudio off
if (next.format === "gif") {
next.keepAudio = false;
}
return next;
});
}, []);

useEffect(() => {
if (typeof window === "undefined") return;

Expand All @@ -280,6 +287,10 @@ export function useVideoEditor() {
if (decoded) {
setRecipe(migratePersistedRecipe(decoded));
return;
} else {
const url = new URL(window.location.href);
url.searchParams.delete("settings");
window.history.replaceState(null, "", url.toString());
}
}

Expand Down
36 changes: 34 additions & 2 deletions src/lib/editorPersistence.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DEFAULT_RECIPE } from "@/lib/constants";
import { EditRecipe, OverlayPosition, isValidRecipe } from "@/lib/types";
import { EditRecipe, OverlayPosition, isValidRecipe, TextOverlay } from "@/lib/types";
import { generateTextOverlayId } from "@/lib/text-overlay";

export const RECIPE_STORAGE_KEY = "reframe:recipe";
export const LEGACY_SETTINGS_KEY = "reframe-settings";
Expand All @@ -12,11 +13,42 @@ export interface OverlayEditorState {
overlayOpacity?: number;
}

function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}

const VALID_FONT_WEIGHTS = ["normal", "bold", "900"];

function sanitizeTextOverlay(overlay: any): TextOverlay {
return {
id: typeof overlay.id === "string" ? overlay.id : generateTextOverlayId(),
text: typeof overlay.text === "string" ? overlay.text.slice(0, 500) : "",
x: typeof overlay.x === "number" && isFinite(overlay.x) ? clamp(overlay.x, 0, 100) : 50,
y: typeof overlay.y === "number" && isFinite(overlay.y) ? clamp(overlay.y, 0, 100) : 20,
fontSize: typeof overlay.fontSize === "number" && isFinite(overlay.fontSize) ? clamp(overlay.fontSize, 12, 120) : 48,
color: typeof overlay.color === "string" && /^#[0-9A-Fa-f]{6}$/.test(overlay.color) ? overlay.color : "#ffffff",
fontWeight: VALID_FONT_WEIGHTS.includes(overlay.fontWeight) ? (overlay.fontWeight as "normal" | "bold" | "900") : "normal",
fontFamily: typeof overlay.fontFamily === "string" ? overlay.fontFamily : "Arial",
fontPath: typeof overlay.fontPath === "string" ? overlay.fontPath : undefined,
};
}

export function migrateRecipe(recipe: Partial<EditRecipe>): EditRecipe {
const rotateValue = [0, 90, 180, 270].includes(recipe.rotate as any) ? (recipe.rotate as 0 | 90 | 180 | 270) : DEFAULT_RECIPE.rotate;

return {
...DEFAULT_RECIPE,
...recipe,
textOverlays: Array.isArray(recipe.textOverlays) ? recipe.textOverlays : [],
quality: typeof recipe.quality === "number" ? clamp(recipe.quality, 18, 30) : DEFAULT_RECIPE.quality,
speed: typeof recipe.speed === "number" ? clamp(recipe.speed, 0.25, 4) : DEFAULT_RECIPE.speed,
brightness: typeof recipe.brightness === "number" ? clamp(recipe.brightness, -1, 1) : DEFAULT_RECIPE.brightness,
contrast: typeof recipe.contrast === "number" ? clamp(recipe.contrast, 0, 2) : DEFAULT_RECIPE.contrast,
saturation: typeof recipe.saturation === "number" ? clamp(recipe.saturation, 0, 3) : DEFAULT_RECIPE.saturation,
customWidth: typeof recipe.customWidth === "number" ? clamp(recipe.customWidth, 16, 7680) : DEFAULT_RECIPE.customWidth,
customHeight: typeof recipe.customHeight === "number" ? clamp(recipe.customHeight, 16, 7680) : DEFAULT_RECIPE.customHeight,
trimStart: typeof recipe.trimStart === "number" ? Math.max(0, recipe.trimStart) : DEFAULT_RECIPE.trimStart,
rotate: rotateValue,
textOverlays: Array.isArray(recipe.textOverlays) ? recipe.textOverlays.map(sanitizeTextOverlay) : [],
};
}

Expand Down
11 changes: 11 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,16 @@ export function isValidRecipe(value: unknown): value is EditRecipe {
if (typeof v.soundOnCompletion !== "boolean") return false;
if (!Array.isArray(v.textOverlays)) return false;

for (const overlay of v.textOverlays) {
if (!overlay || typeof overlay !== "object") return false;
if (typeof overlay.id !== "string") return false;
if (typeof overlay.text !== "string") return false;
if (typeof overlay.x !== "number" || !isFinite(overlay.x) || overlay.x < 0 || overlay.x > 100) return false;
if (typeof overlay.y !== "number" || !isFinite(overlay.y) || overlay.y < 0 || overlay.y > 100) return false;
if (typeof overlay.fontSize !== "number" || !isFinite(overlay.fontSize) || overlay.fontSize < 12 || overlay.fontSize > 120) return false;
if (typeof overlay.color !== "string") return false;
if (!["normal", "bold", "900"].includes(overlay.fontWeight)) return false;
}

return true;
}
Loading