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
62 changes: 41 additions & 21 deletions src/components/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -722,22 +735,29 @@ export default function VideoEditor() {
</div>
</AccordionSection>

<div className="pt-2 flex justify-center items-center gap-6">
<button
type="button"
onClick={handleCopyLink}
className="flex items-center gap-1.5 text-xs font-heading font-bold uppercase tracking-widest text-film-500 hover:text-film-600 hover:opacity-100 transition-all cursor-pointer"
>
<Copy size={12} />
{shareCopied ? "Copied!" : "Copy Link"}
</button>
<button
type="button"
onClick={resetSettings}
className="text-sm font-heading font-bold uppercase tracking-widest text-[var(--muted)] hover:text-film-600 transition-all opacity-60 hover:opacity-100"
>
Reset all settings
</button>
<div className="pt-2 flex flex-col justify-center items-center gap-2">
<div className="flex justify-center items-center gap-6">
<button
type="button"
onClick={handleCopyLink}
className="flex items-center gap-1.5 text-xs font-heading font-bold uppercase tracking-widest text-film-500 hover:text-film-600 hover:opacity-100 transition-all cursor-pointer"
>
<Copy size={12} />
{shareCopied ? "Copied!" : "Copy Link"}
</button>
<button
type="button"
onClick={resetSettings}
className="text-sm font-heading font-bold uppercase tracking-widest text-[var(--muted)] hover:text-film-600 transition-all opacity-60 hover:opacity-100"
>
Reset all settings
</button>
</div>
{shareCopied && (
<span className="text-[10px] text-green-500 animate-fade-in font-heading uppercase tracking-widest text-center mt-1">
✓ Link copied with your settings!
</span>
)}
</div>
</div>

Expand Down
167 changes: 62 additions & 105 deletions src/hooks/useVideoEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,76 @@ export function useVideoEditor() {
} | null>(null);
const [recipe, setRecipe] = useState<EditRecipe>(() => {
if (typeof window === "undefined") return { ...DEFAULT_RECIPE };

let savedRecipe: Partial<EditRecipe> = {};
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<EditRecipe> = {};
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<ExportStatus>("idle");
Expand Down Expand Up @@ -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<keyof EditRecipe>;
const hasRecipeParams = recipeKeys.some(key => params.has(key));

if (hasRecipeParams) {
const updatedPatch: Partial<EditRecipe> = {};
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<keyof EditRecipe>;

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 {
Expand Down