mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
Add custom aspect ratio inputs and persistence
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ASPECT_RATIOS, type AspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import { ASPECT_RATIOS, type AspectRatio, isCustomAspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter";
|
||||
import { WALLPAPER_PATHS } from "@/lib/wallpapers";
|
||||
import {
|
||||
@@ -331,7 +331,11 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
trimRegions: normalizedTrimRegions,
|
||||
speedRegions: normalizedSpeedRegions,
|
||||
annotationRegions: normalizedAnnotationRegions,
|
||||
aspectRatio: editor.aspectRatio && validAspectRatios.has(editor.aspectRatio) ? editor.aspectRatio : "16:9",
|
||||
aspectRatio:
|
||||
typeof editor.aspectRatio === "string" &&
|
||||
(validAspectRatios.has(editor.aspectRatio as AspectRatio) || isCustomAspectRatio(editor.aspectRatio))
|
||||
? (editor.aspectRatio as AspectRatio)
|
||||
: "16:9",
|
||||
exportQuality: editor.exportQuality === "medium" || editor.exportQuality === "source" ? editor.exportQuality : "good",
|
||||
exportFormat: editor.exportFormat === "gif" ? "gif" : "mp4",
|
||||
gifFrameRate:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type WheelEvent } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type WheelEvent } from "react";
|
||||
import { useTimelineContext } from "dnd-timeline";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Scissors, ZoomIn, MessageSquare, ChevronDown, Check, Gauge, WandSparkles } from "lucide-react";
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { type AspectRatio, getAspectRatioLabel, ASPECT_RATIOS } from "@/utils/aspectRatioUtils";
|
||||
import { type AspectRatio, getAspectRatioLabel, ASPECT_RATIOS, isCustomAspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import { formatShortcut } from "@/utils/platformUtils";
|
||||
import { TutorialHelp } from "../TutorialHelp";
|
||||
import { useShortcuts } from "@/contexts/ShortcutsContext";
|
||||
@@ -610,6 +610,8 @@ export default function TimelineEditor({
|
||||
const [range, setRange] = useState<Range>(() => createInitialRange(totalMs));
|
||||
const [keyframes, setKeyframes] = useState<{ id: string; time: number }[]>([]);
|
||||
const [selectedKeyframeId, setSelectedKeyframeId] = useState<string | null>(null);
|
||||
const [customAspectWidth, setCustomAspectWidth] = useState('16');
|
||||
const [customAspectHeight, setCustomAspectHeight] = useState('9');
|
||||
const [scrollLabels, setScrollLabels] = useState({
|
||||
pan: 'Shift + Ctrl + Scroll',
|
||||
zoom: 'Ctrl + Scroll'
|
||||
@@ -617,6 +619,36 @@ export default function TimelineEditor({
|
||||
const timelineContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { shortcuts: keyShortcuts, isMac } = useShortcuts();
|
||||
|
||||
useEffect(() => {
|
||||
if (aspectRatio === 'native') {
|
||||
return;
|
||||
}
|
||||
const [width, height] = aspectRatio.split(':');
|
||||
if (width && height) {
|
||||
setCustomAspectWidth(width);
|
||||
setCustomAspectHeight(height);
|
||||
}
|
||||
}, [aspectRatio]);
|
||||
|
||||
const applyCustomAspectRatio = useCallback(() => {
|
||||
const width = Number.parseInt(customAspectWidth, 10);
|
||||
const height = Number.parseInt(customAspectHeight, 10);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
||||
toast.error('Custom aspect ratio must be positive numbers.');
|
||||
return;
|
||||
}
|
||||
onAspectRatioChange(`${width}:${height}` as AspectRatio);
|
||||
}, [customAspectHeight, customAspectWidth, onAspectRatioChange]);
|
||||
|
||||
const handleCustomAspectRatioKeyDown = useCallback((event: ReactKeyboardEvent<HTMLInputElement>) => {
|
||||
// Prevent Radix DropdownMenu typeahead from selecting preset items while typing.
|
||||
event.stopPropagation();
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
applyCustomAspectRatio();
|
||||
}
|
||||
}, [applyCustomAspectRatio]);
|
||||
|
||||
useEffect(() => {
|
||||
formatShortcut(['shift', 'mod', 'Scroll']).then(pan => {
|
||||
formatShortcut(['mod', 'Scroll']).then(zoom => {
|
||||
@@ -1243,6 +1275,38 @@ export default function TimelineEditor({
|
||||
{aspectRatio === ratio && <Check className="w-3 h-3 text-[#2563EB]" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<div className="mx-1 my-1 h-px bg-white/10" />
|
||||
<div className="px-2 py-1.5 flex items-center gap-2 text-slate-300">
|
||||
<span className="text-sm">Custom</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={customAspectWidth}
|
||||
onChange={(event) => setCustomAspectWidth(event.target.value.replace(/\D/g, ''))}
|
||||
onKeyDown={handleCustomAspectRatioKeyDown}
|
||||
className="w-12 h-7 rounded border border-white/15 bg-black/20 px-1.5 text-sm text-slate-100 focus:outline-none focus:ring-1 focus:ring-[#2563EB]"
|
||||
aria-label="Custom aspect width"
|
||||
/>
|
||||
<span className="text-slate-500">:</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={customAspectHeight}
|
||||
onChange={(event) => setCustomAspectHeight(event.target.value.replace(/\D/g, ''))}
|
||||
onKeyDown={handleCustomAspectRatioKeyDown}
|
||||
className="w-12 h-7 rounded border border-white/15 bg-black/20 px-1.5 text-sm text-slate-100 focus:outline-none focus:ring-1 focus:ring-[#2563EB]"
|
||||
aria-label="Custom aspect height"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={applyCustomAspectRatio}
|
||||
className="h-7 px-2 text-xs text-slate-300 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
Set
|
||||
</Button>
|
||||
{isCustomAspectRatio(aspectRatio) && <Check className="w-3 h-3 text-[#2563EB] ml-auto" />}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="w-[1px] h-4 bg-white/10" />
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
export const ASPECT_RATIOS = ['native', '16:9', '9:16', '1:1', '4:3', '4:5', '16:10', '10:16'] as const;
|
||||
|
||||
export type AspectRatio = typeof ASPECT_RATIOS[number];
|
||||
type PresetAspectRatio = typeof ASPECT_RATIOS[number];
|
||||
type CustomAspectRatio = `${number}:${number}`;
|
||||
|
||||
export type AspectRatio = PresetAspectRatio | CustomAspectRatio;
|
||||
|
||||
const CUSTOM_ASPECT_RATIO_REGEX = /^(\d+):(\d+)$/;
|
||||
|
||||
export function isCustomAspectRatio(aspectRatio: string): aspectRatio is CustomAspectRatio {
|
||||
if ((ASPECT_RATIOS as readonly string[]).includes(aspectRatio)) {
|
||||
return false;
|
||||
}
|
||||
const match = aspectRatio.match(CUSTOM_ASPECT_RATIO_REGEX);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
const width = Number(match[1]);
|
||||
const height = Number(match[2]);
|
||||
return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0;
|
||||
}
|
||||
|
||||
function parseCustomAspectRatioValue(aspectRatio: string): number | null {
|
||||
if (!isCustomAspectRatio(aspectRatio)) {
|
||||
return null;
|
||||
}
|
||||
const [widthText, heightText] = aspectRatio.split(':');
|
||||
const width = Number(widthText);
|
||||
const height = Number(heightText);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
return width / height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the numeric value of an aspect ratio.
|
||||
@@ -17,11 +48,8 @@ export function getAspectRatioValue(aspectRatio: AspectRatio, nativeAspectRatio
|
||||
case '4:5': return 4 / 5;
|
||||
case '16:10': return 16 / 10;
|
||||
case '10:16': return 10 / 16;
|
||||
default: {
|
||||
// Ensures all cases are handled - TypeScript errors if missing
|
||||
const _exhaustiveCheck: never = aspectRatio;
|
||||
return _exhaustiveCheck;
|
||||
}
|
||||
default:
|
||||
return parseCustomAspectRatioValue(aspectRatio) ?? 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +69,9 @@ export function getAspectRatioLabel(aspectRatio: AspectRatio): string {
|
||||
if (aspectRatio === 'native') {
|
||||
return 'Native';
|
||||
}
|
||||
if (isCustomAspectRatio(aspectRatio)) {
|
||||
return `Custom ${aspectRatio}`;
|
||||
}
|
||||
return aspectRatio;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user