diff options
Diffstat (limited to 'apps/web/src/components/editor/export-button.tsx')
| -rw-r--r-- | apps/web/src/components/editor/export-button.tsx | 136 |
1 files changed, 62 insertions, 74 deletions
diff --git a/apps/web/src/components/editor/export-button.tsx b/apps/web/src/components/editor/export-button.tsx index bc44e650..ddf8308d 100644 --- a/apps/web/src/components/editor/export-button.tsx +++ b/apps/web/src/components/editor/export-button.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef } from "react"; +import { useState } from "react"; import { TransitionTopIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { @@ -14,35 +14,47 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Progress } from "@/components/ui/progress"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/utils/ui"; -import { getExportMimeType, getExportFileExtension } from "@/lib/export"; +import { getExportMimeType, getExportFileExtension, downloadBuffer } from "@/lib/export"; import { Check, Copy, Download, RotateCcw } from "lucide-react"; import { EXPORT_FORMAT_VALUES, EXPORT_QUALITY_VALUES, type ExportFormat, type ExportQuality, - type ExportResult, } from "@/types/export"; import { Section, SectionContent, SectionHeader, + SectionTitle, } from "@/components/editor/panels/properties/section"; import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_EXPORT_OPTIONS } from "@/constants/export-constants"; +function isExportFormat(value: string): value is ExportFormat { + return EXPORT_FORMAT_VALUES.some((formatValue) => formatValue === value); +} + +function isExportQuality(value: string): value is ExportQuality { + return EXPORT_QUALITY_VALUES.some((qualityValue) => qualityValue === value); +} + export function ExportButton() { const [isExportPopoverOpen, setIsExportPopoverOpen] = useState(false); const editor = useEditor(); - const handleExport = () => { - setIsExportPopoverOpen(true); - }; + const hasProject = !!editor.project.getActiveOrNull(); - const hasProject = !!editor.project.getActive(); + const handlePopoverOpenChange = ({ open }: { open: boolean }) => { + if (!open) { + editor.project.cancelExport(); + editor.project.clearExportState(); + } + setIsExportPopoverOpen(open); + }; return ( - <Popover open={isExportPopoverOpen} onOpenChange={setIsExportPopoverOpen}> + <Popover open={isExportPopoverOpen} onOpenChange={(open) => handlePopoverOpenChange({ open })}> <PopoverTrigger asChild> <button type="button" @@ -50,12 +62,12 @@ export function ExportButton() { "flex items-center gap-1.5 rounded-md bg-[#38BDF8] px-[0.12rem] py-[0.12rem] text-white", hasProject ? "cursor-pointer" : "cursor-not-allowed opacity-50", )} - onClick={hasProject ? handleExport : undefined} + onClick={hasProject ? () => setIsExportPopoverOpen(true) : undefined} disabled={!hasProject} onKeyDown={(event) => { if (hasProject && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); - handleExport(); + setIsExportPopoverOpen(true); } }} > @@ -80,71 +92,49 @@ function ExportPopover({ }) { const editor = useEditor(); const activeProject = editor.project.getActive(); + const { isExporting, progress, result: exportResult } = + editor.project.getExportState(); const [format, setFormat] = useState<ExportFormat>( DEFAULT_EXPORT_OPTIONS.format, ); const [quality, setQuality] = useState<ExportQuality>( DEFAULT_EXPORT_OPTIONS.quality, ); - const [includeAudio, setIncludeAudio] = useState<boolean>( - DEFAULT_EXPORT_OPTIONS.includeAudio || true, + const [shouldIncludeAudio, setShouldIncludeAudio] = useState<boolean>( + DEFAULT_EXPORT_OPTIONS.includeAudio ?? true, ); - const [isExporting, setIsExporting] = useState(false); - const [progress, setProgress] = useState(0); - const [exportResult, setExportResult] = useState<ExportResult | null>(null); - const cancelRequestedRef = useRef(false); const handleExport = async () => { if (!activeProject) return; - cancelRequestedRef.current = false; - setIsExporting(true); - setProgress(0); - setExportResult(null); - const result = await editor.project.export({ options: { - format, - quality, - fps: activeProject.settings.fps, - includeAudio, - onProgress: ({ progress }) => setProgress(progress), - onCancel: () => cancelRequestedRef.current, + format, + quality, + fps: activeProject.settings.fps, + includeAudio: shouldIncludeAudio, }, }); - setIsExporting(false); - if (result.cancelled) { - setExportResult(null); - setProgress(0); + editor.project.clearExportState(); return; } - setExportResult(result); - if (result.success && result.buffer) { - const mimeType = getExportMimeType({ format }); - const extension = getExportFileExtension({ format }); - const blob = new Blob([result.buffer], { type: mimeType }); - const url = URL.createObjectURL(blob); - - const a = document.createElement("a"); - a.href = url; - a.download = `${activeProject.metadata.name}${extension}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + downloadBuffer({ + buffer: result.buffer, + filename: `${activeProject.metadata.name}${getExportFileExtension({ format })}`, + mimeType: getExportMimeType({ format }), + }); + editor.project.clearExportState(); onOpenChange(false); - setExportResult(null); - setProgress(0); } }; const handleCancel = () => { - cancelRequestedRef.current = true; + editor.project.cancelExport(); }; return ( @@ -166,8 +156,10 @@ function ExportPopover({ {!isExporting && ( <> <div className="flex flex-col"> - <Section collapsible defaultOpen={false} hasBorderTop={false}> - <SectionHeader title="Format" /> + <Section collapsible defaultOpen={false} showTopBorder={false}> + <SectionHeader> + <SectionTitle>Format</SectionTitle> + </SectionHeader> <SectionContent> <RadioGroup value={format} @@ -194,7 +186,9 @@ function ExportPopover({ </Section> <Section collapsible defaultOpen={false}> - <SectionHeader title="Quality" /> + <SectionHeader> + <SectionTitle>Quality</SectionTitle> + </SectionHeader> <SectionContent> <RadioGroup value={quality} @@ -219,7 +213,7 @@ function ExportPopover({ <div className="flex items-center space-x-2"> <RadioGroupItem value="very_high" id="very_high" /> <Label htmlFor="very_high"> - Very High - Largest file size + Very high - Largest file size </Label> </div> </RadioGroup> @@ -227,15 +221,17 @@ function ExportPopover({ </Section> <Section collapsible defaultOpen={false}> - <SectionHeader title="Audio" /> + <SectionHeader> + <SectionTitle>Audio</SectionTitle> + </SectionHeader> <SectionContent> <div className="flex items-center space-x-2"> <Checkbox id="include-audio" - checked={includeAudio} - onCheckedChange={(checked) => - setIncludeAudio(!!checked) - } + checked={shouldIncludeAudio} + onCheckedChange={(checked) => + setShouldIncludeAudio(!!checked) + } /> <Label htmlFor="include-audio"> Include audio in export @@ -256,15 +252,15 @@ function ExportPopover({ {isExporting && ( <div className="space-y-4 p-3"> - <div className="flex flex-col"> - <div className="flex items-center justify-between text-center"> - <p className="text-muted-foreground mb-2 text-sm"> - {Math.round(progress * 100)}% - </p> - <p className="text-muted-foreground mb-2 text-sm">100%</p> - </div> - <Progress value={progress * 100} className="w-full" /> + <div className="flex flex-col gap-2"> + <div className="flex items-center justify-between text-center"> + <p className="text-muted-foreground text-sm"> + {Math.round(progress * 100)}% + </p> + <p className="text-muted-foreground text-sm">100%</p> </div> + <Progress value={progress * 100} className="w-full" /> + </div> <Button variant="outline" @@ -282,14 +278,6 @@ function ExportPopover({ ); } -function isExportFormat(value: string): value is ExportFormat { - return EXPORT_FORMAT_VALUES.some((formatValue) => formatValue === value); -} - -function isExportQuality(value: string): value is ExportQuality { - return EXPORT_QUALITY_VALUES.some((qualityValue) => qualityValue === value); -} - function ExportError({ error, onRetry, @@ -306,7 +294,7 @@ function ExportError({ }; return ( - <div className="space-y-4"> + <div className="space-y-4 p-3"> <div className="flex flex-col gap-1.5"> <p className="text-destructive text-sm font-medium">Export failed</p> <p className="text-muted-foreground text-xs">{error}</p> |
