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 {
|
import {
|
||||||
EXPORT_FORMAT_VALUES,
|
EXPORT_FORMAT_VALUES,
|
||||||
EXPORT_QUALITY_VALUES,
|
EXPORT_QUALITY_VALUES,
|
||||||
|
PLATFORM_PRESET_VALUES,
|
||||||
|
PLATFORM_PRESETS,
|
||||||
type ExportFormat,
|
type ExportFormat,
|
||||||
type ExportQuality,
|
type ExportQuality,
|
||||||
|
type PlatformPreset,
|
||||||
} from "@/lib/export";
|
} from "@/lib/export";
|
||||||
import {
|
import {
|
||||||
Section,
|
Section,
|
||||||
|
|
@ -43,6 +46,12 @@ function isExportQuality(value: string): value is ExportQuality {
|
||||||
return EXPORT_QUALITY_VALUES.some((qualityValue) => qualityValue === value);
|
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() {
|
export function ExportButton() {
|
||||||
const [isExportPopoverOpen, setIsExportPopoverOpen] = useState(false);
|
const [isExportPopoverOpen, setIsExportPopoverOpen] = useState(false);
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
|
|
@ -101,6 +110,8 @@ function ExportPopover({
|
||||||
const activeProject = useEditor((e) => e.project.getActive());
|
const activeProject = useEditor((e) => e.project.getActive());
|
||||||
const exportState = useEditor((e) => e.project.getExportState());
|
const exportState = useEditor((e) => e.project.getExportState());
|
||||||
const { isExporting, progress, result: exportResult } = exportState;
|
const { isExporting, progress, result: exportResult } = exportState;
|
||||||
|
const [platformPreset, setPlatformPreset] =
|
||||||
|
useState<PlatformPreset>("custom");
|
||||||
const [format, setFormat] = useState<ExportFormat>(
|
const [format, setFormat] = useState<ExportFormat>(
|
||||||
DEFAULT_EXPORT_OPTIONS.format,
|
DEFAULT_EXPORT_OPTIONS.format,
|
||||||
);
|
);
|
||||||
|
|
@ -111,15 +122,32 @@ function ExportPopover({
|
||||||
DEFAULT_EXPORT_OPTIONS.includeAudio ?? true,
|
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 () => {
|
const handleExport = async () => {
|
||||||
if (!activeProject) return;
|
if (!activeProject) return;
|
||||||
|
|
||||||
|
const presetConfig =
|
||||||
|
platformPreset !== "custom"
|
||||||
|
? PLATFORM_PRESETS[platformPreset]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const result = await editor.project.export({
|
const result = await editor.project.export({
|
||||||
options: {
|
options: {
|
||||||
format,
|
format,
|
||||||
quality,
|
quality,
|
||||||
fps: activeProject.settings.fps,
|
fps: activeProject.settings.fps,
|
||||||
includeAudio: shouldIncludeAudio,
|
includeAudio: shouldIncludeAudio,
|
||||||
|
width: presetConfig?.width,
|
||||||
|
height: presetConfig?.height,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -163,6 +191,63 @@ function ExportPopover({
|
||||||
{!isExporting && (
|
{!isExporting && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col">
|
<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
|
<Section
|
||||||
collapsible
|
collapsible
|
||||||
defaultOpen={false}
|
defaultOpen={false}
|
||||||
|
|
@ -177,6 +262,7 @@ function ExportPopover({
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (isExportFormat(value)) {
|
if (isExportFormat(value)) {
|
||||||
setFormat(value);
|
setFormat(value);
|
||||||
|
setPlatformPreset("custom");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -206,6 +292,7 @@ function ExportPopover({
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (isExportQuality(value)) {
|
if (isExportQuality(value)) {
|
||||||
setQuality(value);
|
setQuality(value);
|
||||||
|
setPlatformPreset("custom");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ export class RendererManager {
|
||||||
onProgress?: ({ progress }: { progress: number }) => void;
|
onProgress?: ({ progress }: { progress: number }) => void;
|
||||||
onCancel?: () => boolean;
|
onCancel?: () => boolean;
|
||||||
}): Promise<ExportResult> {
|
}): Promise<ExportResult> {
|
||||||
const { format, quality, fps, includeAudio } = options;
|
const { format, quality, fps, includeAudio, width: overrideWidth, height: overrideHeight } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tracks = this.editor.scenes.getActiveScene().tracks;
|
const tracks = this.editor.scenes.getActiveScene().tracks;
|
||||||
|
|
@ -153,7 +153,10 @@ export class RendererManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportFps = fps ?? activeProject.settings.fps;
|
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;
|
let audioBuffer: AudioBuffer | null = null;
|
||||||
if (includeAudio) {
|
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 ExportFormat = (typeof EXPORT_FORMAT_VALUES)[number];
|
||||||
export type ExportQuality = (typeof EXPORT_QUALITY_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 {
|
export interface ExportOptions {
|
||||||
format: ExportFormat;
|
format: ExportFormat;
|
||||||
quality: ExportQuality;
|
quality: ExportQuality;
|
||||||
fps?: FrameRate;
|
fps?: FrameRate;
|
||||||
includeAudio?: boolean;
|
includeAudio?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportResult {
|
export interface ExportResult {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue