diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx
index b1d2e574..3293045b 100644
--- a/src/components/VideoEditor.tsx
+++ b/src/components/VideoEditor.tsx
@@ -251,11 +251,24 @@ export default function VideoEditor() {
const handleCopyLink = () => {
if (typeof window === "undefined") return;
- const encoded = btoa(JSON.stringify(recipe));
- const url = new URL(window.location.href);
- url.searchParams.set("settings", encoded);
- history.replaceState(null, "", url.toString());
- navigator.clipboard.writeText(url.toString()).then(() => {
+ const params = new URLSearchParams();
+
+ if (recipe.preset !== "vertical-9-16") params.set('preset', recipe.preset);
+ if (recipe.framing !== "fit") params.set('framing', recipe.framing);
+ if (recipe.trimStart !== 0) params.set('trimStart', String(recipe.trimStart));
+ if (recipe.trimEnd !== null) params.set('trimEnd', String(recipe.trimEnd));
+ if (recipe.speed !== 1) params.set('speed', String(recipe.speed));
+ if (recipe.rotate !== 0) params.set('rotate', String(recipe.rotate));
+ if (recipe.quality !== 23) params.set('quality', String(recipe.quality));
+ if (recipe.keepAudio !== true) params.set('keepAudio', String(recipe.keepAudio));
+ if (recipe.format !== "mp4") params.set('format', recipe.format);
+ if (recipe.brightness !== 0) params.set('brightness', String(recipe.brightness));
+ if (recipe.contrast !== 1) params.set('contrast', String(recipe.contrast));
+ if (recipe.saturation !== 1) params.set('saturation', String(recipe.saturation));
+
+ const shareUrl = `${window.location.origin}${window.location.pathname}?${params.toString()}`;
+ history.replaceState(null, "", shareUrl);
+ navigator.clipboard.writeText(shareUrl).then(() => {
setShareCopied(true);
setTimeout(() => setShareCopied(false), 2000);
});
@@ -722,22 +735,29 @@ export default function VideoEditor() {
-
-
-
+
+
+
+
+
+ {shareCopied && (
+
+ ✓ Link copied with your settings!
+
+ )}
diff --git a/src/hooks/useVideoEditor.ts b/src/hooks/useVideoEditor.ts
index b3ea8283..43e44b5f 100644
--- a/src/hooks/useVideoEditor.ts
+++ b/src/hooks/useVideoEditor.ts
@@ -154,18 +154,76 @@ export function useVideoEditor() {
} | null>(null);
const [recipe, setRecipe] = useState(() => {
if (typeof window === "undefined") return { ...DEFAULT_RECIPE };
+
+ let savedRecipe: Partial = {};
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ if (isValidRecipe(parsed)) {
+ savedRecipe = parsed;
+ }
+ } else {
+ const legacySaved = localStorage.getItem("reframe-settings");
+ if (legacySaved) {
+ const parsed = JSON.parse(legacySaved);
+ const sanitizeDimension = (val: unknown, fallback: number): number => {
+ const n = Number(val);
+ return Number.isFinite(n) && n >= 16 && n <= 7680 ? n : fallback;
+ };
+ savedRecipe = {
+ preset: parsed.preset,
+ quality: parsed.quality,
+ speed: parsed.speed,
+ customWidth: parsed.customWidth ? sanitizeDimension(parsed.customWidth, DEFAULT_RECIPE.customWidth) : undefined,
+ customHeight: parsed.customHeight ? sanitizeDimension(parsed.customHeight, DEFAULT_RECIPE.customHeight) : undefined,
+ };
+ }
+ }
+ } catch {
+ // ignore
+ }
+
const params = new URLSearchParams(window.location.search);
+
+ // Legacy support for base64 encoded settings
const encoded = params.get("settings");
if (encoded) {
const decoded = decodeRecipe(encoded);
if (decoded) {
- return migrateRecipe(decoded);
+ return migrateRecipe({ ...savedRecipe, ...decoded });
}
}
+
+ // Read individual params from "Copy Link" functionality
+ const parsedParams: Partial = {};
+ if (params.has("preset")) parsedParams.preset = params.get("preset") as string;
+ if (params.has("framing")) parsedParams.framing = params.get("framing") as "fit" | "fill";
+ if (params.has("trimStart")) parsedParams.trimStart = Number(params.get("trimStart"));
+ if (params.has("trimEnd")) parsedParams.trimEnd = params.get("trimEnd") === "null" ? null : Number(params.get("trimEnd"));
+ if (params.has("speed")) parsedParams.speed = Number(params.get("speed"));
+ if (params.has("rotate")) parsedParams.rotate = Number(params.get("rotate")) as 0 | 90 | 180 | 270;
+ if (params.has("quality")) parsedParams.quality = Number(params.get("quality"));
+ if (params.has("keepAudio")) parsedParams.keepAudio = params.get("keepAudio") === "true";
+ if (params.has("format")) parsedParams.format = params.get("format") as "mp4" | "webm" | "mkv" | "gif";
+ if (params.has("brightness")) parsedParams.brightness = Number(params.get("brightness"));
+ if (params.has("contrast")) parsedParams.contrast = Number(params.get("contrast"));
+ if (params.has("saturation")) parsedParams.saturation = Number(params.get("saturation"));
+
+ Object.keys(parsedParams).forEach(key => {
+ const k = key as keyof EditRecipe;
+ if (parsedParams[k] === undefined || (typeof parsedParams[k] === 'number' && isNaN(parsedParams[k] as number))) {
+ delete parsedParams[k];
+ }
+ });
+
+ const soundOnCompletion = localStorage.getItem("soundOnCompletion") === "true";
+
return migrateRecipe({
- soundOnCompletion:
- typeof window !== "undefined" &&
- localStorage.getItem("soundOnCompletion") === "true",
+ ...DEFAULT_RECIPE,
+ ...savedRecipe,
+ ...parsedParams,
+ soundOnCompletion,
});
});
const [status, setStatus] = useState("idle");
@@ -231,107 +289,6 @@ export function useVideoEditor() {
}
};
- useEffect(() => {
- if (typeof window === "undefined") return;
- try {
- const params = new URLSearchParams(window.location.search);
- const recipeKeys = Object.keys(DEFAULT_RECIPE) as Array;
- const hasRecipeParams = recipeKeys.some(key => params.has(key));
-
- if (hasRecipeParams) {
- const updatedPatch: Partial = {};
- recipeKeys.forEach((key) => {
- const paramVal = params.get(key);
- if (paramVal !== null) {
- const defaultType = typeof DEFAULT_RECIPE[key];
- let parsedVal: any;
-
- if (defaultType === "number") {
- parsedVal = parseFloat(paramVal);
- } else if (defaultType === "boolean") {
- parsedVal = paramVal === "true";
- } else {
- parsedVal = paramVal === "null" ? null : paramVal;
- }
-
- if (isValidValue(key, parsedVal)) {
- (updatedPatch as any)[key] = parsedVal;
- }
- }
- });
-
- if (Object.keys(updatedPatch).length > 0) {
- setRecipe(prev => ({
- ...prev,
- ...updatedPatch
- }));
- }
- } else {
- // Try full recipe restore first (new key)
- try {
- const raw = localStorage.getItem(STORAGE_KEY);
- if (raw) {
- const parsed = JSON.parse(raw);
- if (isValidRecipe(parsed)) {
- setRecipe(parsed);
- return;
- }
- }
- } catch {
- // ignore parse/validation errors and fall back to legacy
- }
-
- // Legacy partial settings (keep for backward compatibility)
- const saved = localStorage.getItem("reframe-settings");
- if (saved) {
- const parsed = JSON.parse(saved);
- const sanitizeDimension = (val: unknown, fallback: number): number => {
- const n = Number(val);
- return Number.isFinite(n) && n >= 16 && n <= 7680 ? n : fallback;
- };
- setRecipe(prev => ({
- ...prev,
- preset: parsed.preset ?? prev.preset,
- quality: parsed.quality ?? prev.quality,
- speed: parsed.speed ?? prev.speed,
- customWidth: sanitizeDimension(parsed.customWidth, prev.customWidth),
- customHeight: sanitizeDimension(parsed.customHeight, prev.customHeight),
- }));
- }
- }
- } catch (e) {
- // ignore
- }
- }, []);
-
- useEffect(() => {
- if (typeof window === "undefined") return;
- try {
- const params = new URLSearchParams();
- const recipeKeys = Object.keys(DEFAULT_RECIPE) as Array;
-
- recipeKeys.forEach((key) => {
- const currentVal = recipe[key];
- const defaultVal = DEFAULT_RECIPE[key];
-
- if (currentVal !== defaultVal) {
- params.set(key, currentVal === null ? "null" : String(currentVal));
- }
- });
-
- const newQuery = params.toString();
- const currentQuery = window.location.search.replace(/^\?/, "");
-
- if (newQuery !== currentQuery) {
- const newUrl = newQuery
- ? `${window.location.pathname}?${newQuery}`
- : window.location.pathname;
- window.history.replaceState(null, "", newUrl);
- }
- } catch (e) {
- // ignore
- }
- }, [recipe]);
useEffect(() => {
try {