feat: add platform export presets for YouTube, TikTok, Instagram, Twitter

Add one-click platform presets to the export dialog that auto-set
resolution, format, and quality for common social media platforms:
- YouTube: 1920x1080, H.264, Very High quality
- TikTok: 1080x1920, H.264, High quality
- Instagram Reels: 1080x1920, H.264, High quality
- Instagram Post: 1080x1080, H.264, High quality
- Twitter/X: 1280x720, H.264, High quality

Selecting a platform preset auto-fills format and quality settings.
Manually changing format or quality resets to Custom mode.
Existing quality presets remain unchanged.

Fixes #737
This commit is contained in:
Rene Zander 2026-04-13 10:18:35 +00:00
parent cbd1b82123
commit 8a8b9ed6f5
3 changed files with 154 additions and 2 deletions

View File

@ -23,8 +23,11 @@ import { Check, Copy, Download, RotateCcw } from "lucide-react";
import {
EXPORT_FORMAT_VALUES,
EXPORT_QUALITY_VALUES,
PLATFORM_PRESET_VALUES,
PLATFORM_PRESETS,
type ExportFormat,
type ExportQuality,
type PlatformPreset,
} from "@/lib/export";
import {
Section,
@ -43,6 +46,12 @@ function isExportQuality(value: string): value is ExportQuality {
return EXPORT_QUALITY_VALUES.some((qualityValue) => qualityValue === value);
}
function isPlatformPreset(value: string): value is PlatformPreset {
return PLATFORM_PRESET_VALUES.some(
(presetValue) => presetValue === value,
);
}
export function ExportButton() {
const [isExportPopoverOpen, setIsExportPopoverOpen] = useState(false);
const editor = useEditor();
@ -101,6 +110,8 @@ function ExportPopover({
const activeProject = useEditor((e) => e.project.getActive());
const exportState = useEditor((e) => e.project.getExportState());
const { isExporting, progress, result: exportResult } = exportState;
const [platformPreset, setPlatformPreset] =
useState<PlatformPreset>("custom");
const [format, setFormat] = useState<ExportFormat>(
DEFAULT_EXPORT_OPTIONS.format,
);
@ -111,15 +122,32 @@ function ExportPopover({
DEFAULT_EXPORT_OPTIONS.includeAudio ?? true,
);
const handlePlatformPresetChange = (value: string) => {
if (!isPlatformPreset(value)) return;
setPlatformPreset(value);
if (value !== "custom") {
const preset = PLATFORM_PRESETS[value];
setFormat(preset.format);
setQuality(preset.quality);
}
};
const handleExport = async () => {
if (!activeProject) return;
const presetConfig =
platformPreset !== "custom"
? PLATFORM_PRESETS[platformPreset]
: undefined;
const result = await editor.project.export({
options: {
format,
quality,
fps: activeProject.settings.fps,
includeAudio: shouldIncludeAudio,
width: presetConfig?.width,
height: presetConfig?.height,
},
});
@ -163,6 +191,63 @@ function ExportPopover({
{!isExporting && (
<>
<div className="flex flex-col">
<Section
collapsible
defaultOpen
showTopBorder={false}
>
<SectionHeader>
<SectionTitle>Platform</SectionTitle>
</SectionHeader>
<SectionContent>
<RadioGroup
value={platformPreset}
onValueChange={handlePlatformPresetChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="custom" id="custom" />
<Label htmlFor="custom">Custom</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="youtube" id="youtube" />
<Label htmlFor="youtube">
{PLATFORM_PRESETS.youtube.label}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="tiktok" id="tiktok" />
<Label htmlFor="tiktok">
{PLATFORM_PRESETS.tiktok.label}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="instagram_reels"
id="instagram_reels"
/>
<Label htmlFor="instagram_reels">
{PLATFORM_PRESETS.instagram_reels.label}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="instagram_post"
id="instagram_post"
/>
<Label htmlFor="instagram_post">
{PLATFORM_PRESETS.instagram_post.label}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="twitter" id="twitter" />
<Label htmlFor="twitter">
{PLATFORM_PRESETS.twitter.label}
</Label>
</div>
</RadioGroup>
</SectionContent>
</Section>
<Section
collapsible
defaultOpen={false}
@ -177,6 +262,7 @@ function ExportPopover({
onValueChange={(value) => {
if (isExportFormat(value)) {
setFormat(value);
setPlatformPreset("custom");
}
}}
>
@ -206,6 +292,7 @@ function ExportPopover({
onValueChange={(value) => {
if (isExportQuality(value)) {
setQuality(value);
setPlatformPreset("custom");
}
}}
>

View File

@ -136,7 +136,7 @@ export class RendererManager {
onProgress?: ({ progress }: { progress: number }) => void;
onCancel?: () => boolean;
}): Promise<ExportResult> {
const { format, quality, fps, includeAudio } = options;
const { format, quality, fps, includeAudio, width: overrideWidth, height: overrideHeight } = options;
try {
const tracks = this.editor.scenes.getActiveScene().tracks;
@ -153,7 +153,10 @@ export class RendererManager {
}
const exportFps = fps ?? activeProject.settings.fps;
const canvasSize = activeProject.settings.canvasSize;
const canvasSize = {
width: overrideWidth ?? activeProject.settings.canvasSize.width,
height: overrideHeight ?? activeProject.settings.canvasSize.height,
};
let audioBuffer: AudioBuffer | null = null;
if (includeAudio) {

View File

@ -13,11 +13,73 @@ export const EXPORT_FORMAT_VALUES = ["mp4", "webm"] as const;
export type ExportFormat = (typeof EXPORT_FORMAT_VALUES)[number];
export type ExportQuality = (typeof EXPORT_QUALITY_VALUES)[number];
export const PLATFORM_PRESET_VALUES = [
"custom",
"youtube",
"tiktok",
"instagram_reels",
"instagram_post",
"twitter",
] as const;
export type PlatformPreset = (typeof PLATFORM_PRESET_VALUES)[number];
export interface PlatformPresetConfig {
label: string;
width: number;
height: number;
format: ExportFormat;
quality: ExportQuality;
}
export const PLATFORM_PRESETS: Record<
Exclude<PlatformPreset, "custom">,
PlatformPresetConfig
> = {
youtube: {
label: "YouTube (1920x1080)",
width: 1920,
height: 1080,
format: "mp4",
quality: "very_high",
},
tiktok: {
label: "TikTok (1080x1920)",
width: 1080,
height: 1920,
format: "mp4",
quality: "high",
},
instagram_reels: {
label: "Instagram Reels (1080x1920)",
width: 1080,
height: 1920,
format: "mp4",
quality: "high",
},
instagram_post: {
label: "Instagram Post (1080x1080)",
width: 1080,
height: 1080,
format: "mp4",
quality: "high",
},
twitter: {
label: "Twitter/X (1280x720)",
width: 1280,
height: 720,
format: "mp4",
quality: "high",
},
};
export interface ExportOptions {
format: ExportFormat;
quality: ExportQuality;
fps?: FrameRate;
includeAudio?: boolean;
width?: number;
height?: number;
}
export interface ExportResult {