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:
parent
cbd1b82123
commit
8a8b9ed6f5
|
|
@ -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");
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue