From 7bf0984698e43b9aa2dd879f65951318ed410390 Mon Sep 17 00:00:00 2001 From: Maze <167211895+mazeincoding@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:20:04 +0100 Subject: codebase overhaul (#697) --- apps/web/src/components/editor/export-button.tsx | 588 ++++++++++++----------- 1 file changed, 309 insertions(+), 279 deletions(-) (limited to 'apps/web/src/components/editor/export-button.tsx') diff --git a/apps/web/src/components/editor/export-button.tsx b/apps/web/src/components/editor/export-button.tsx index 246e8b6f..f79710bf 100644 --- a/apps/web/src/components/editor/export-button.tsx +++ b/apps/web/src/components/editor/export-button.tsx @@ -1,317 +1,347 @@ "use client"; -import { useState } from "react"; -import { TransitionUpIcon } from "../icons"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import { Button } from "../ui/button"; -import { Label } from "../ui/label"; -import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; -import { Progress } from "../ui/progress"; -import { Checkbox } from "../ui/checkbox"; -import { cn } from "@/lib/utils"; +import { useState, useRef } from "react"; +import { TransitionTopIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; import { - exportProject, - getExportMimeType, - getExportFileExtension, - DEFAULT_EXPORT_OPTIONS, -} from "@/lib/export"; -import { useProjectStore } from "@/stores/project-store"; + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +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 { Check, Copy, Download, RotateCcw, X } from "lucide-react"; -import { ExportFormat, ExportQuality, ExportResult } from "@/types/export"; -import { PropertyGroup } from "./properties-panel/property-item"; +import { + EXPORT_FORMAT_VALUES, + EXPORT_QUALITY_VALUES, + type ExportFormat, + type ExportQuality, + type ExportResult, +} from "@/types/export"; +import { PropertyGroup } from "@/components/editor/panels/properties/property-item"; +import { useEditor } from "@/hooks/use-editor"; +import { DEFAULT_EXPORT_OPTIONS } from "@/constants/export-constants"; export function ExportButton() { - const [isExportPopoverOpen, setIsExportPopoverOpen] = useState(false); - const { activeProject } = useProjectStore(); + const [isExportPopoverOpen, setIsExportPopoverOpen] = useState(false); + const editor = useEditor(); - const handleExport = () => { - setIsExportPopoverOpen(true); - }; + const handleExport = () => { + setIsExportPopoverOpen(true); + }; - const hasProject = !!activeProject; + const hasProject = !!editor.project.getActive(); - return ( - - - - - {hasProject && } - - ); + return ( + + + + + {hasProject && } + + ); } function ExportPopover({ - onOpenChange, + onOpenChange, }: { - onOpenChange: (open: boolean) => void; + onOpenChange: (open: boolean) => void; }) { - const { activeProject } = useProjectStore(); - const [format, setFormat] = useState( - DEFAULT_EXPORT_OPTIONS.format - ); - const [quality, setQuality] = useState( - DEFAULT_EXPORT_OPTIONS.quality - ); - const [includeAudio, setIncludeAudio] = useState( - DEFAULT_EXPORT_OPTIONS.includeAudio || true - ); - const [isExporting, setIsExporting] = useState(false); - const [progress, setProgress] = useState(0); - const [exportResult, setExportResult] = useState(null); + const editor = useEditor(); + const activeProject = editor.project.getActive(); + const [format, setFormat] = useState( + DEFAULT_EXPORT_OPTIONS.format, + ); + const [quality, setQuality] = useState( + DEFAULT_EXPORT_OPTIONS.quality, + ); + const [includeAudio, setIncludeAudio] = useState( + DEFAULT_EXPORT_OPTIONS.includeAudio || true, + ); + const [isExporting, setIsExporting] = useState(false); + const [progress, setProgress] = useState(0); + const [exportResult, setExportResult] = useState(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, + }, + }); - const handleExport = async () => { - if (!activeProject) return; + setIsExporting(false); - setIsExporting(true); - setProgress(0); - setExportResult(null); + if (result.cancelled) { + setExportResult(null); + setProgress(0); + return; + } - const result = await exportProject({ - format, - quality, - fps: activeProject.fps, - includeAudio, - onProgress: setProgress, - onCancel: () => false, // TODO: Add cancel functionality - }); + setExportResult(result); - setIsExporting(false); - 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); - if (result.success && result.buffer) { - // Download the file - 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); - const a = document.createElement("a"); - a.href = url; - a.download = `${activeProject.name}${extension}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + onOpenChange(false); + setExportResult(null); + setProgress(0); + } + }; - onOpenChange(false); - setExportResult(null); - setProgress(0); - } - }; + const handleClose = () => { + if (!isExporting) { + onOpenChange(false); + setExportResult(null); + setProgress(0); + } + }; - const handleClose = () => { - if (!isExporting) { - onOpenChange(false); - setExportResult(null); - setProgress(0); - } - }; + const handleCancel = () => { + cancelRequestedRef.current = true; + }; - return ( - - <> - {exportResult && !exportResult.success ? ( - - ) : ( - <> -
-

- {isExporting ? "Exporting project" : "Export project"} -

- -
+ return ( + + {exportResult && !exportResult.success ? ( + + ) : ( + <> +
+

+ {isExporting ? "Exporting project" : "Export project"} +

+ +
-
- {!isExporting && ( - <> -
- - - setFormat(value as ExportFormat) - } - > -
- - -
-
- - -
-
-
+
+ {!isExporting && ( + <> +
+ + { + if (isExportFormat(value)) { + setFormat(value); + } + }} + > +
+ + +
+
+ + +
+
+
- - - setQuality(value as ExportQuality) - } - > -
- - -
-
- - -
-
- - -
-
- - -
-
-
+ + { + if (isExportQuality(value)) { + setQuality(value); + } + }} + > +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
- -
- - setIncludeAudio(!!checked) - } - /> - -
-
-
+ +
+ + setIncludeAudio(!!checked) + } + /> + +
+
+
- - - )} + + + )} - {isExporting && ( -
-
-
-

- {Math.round(progress * 100)}% -

-

100%

-
- -
+ {isExporting && ( +
+
+
+

+ {Math.round(progress * 100)}% +

+

100%

+
+ +
+ + +
+ )} +
+ + )} + + ); +} + +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, + error, + onRetry, }: { - error: string; - onRetry: () => void; + error: string; + onRetry: () => void; }) { - const [copied, setCopied] = useState(false); + const [copied, setCopied] = useState(false); - const handleCopy = async () => { - await navigator.clipboard.writeText(error); - setCopied(true); - setTimeout(() => setCopied(false), 1000); - }; + const handleCopy = async () => { + await navigator.clipboard.writeText(error); + setCopied(true); + setTimeout(() => setCopied(false), 1000); + }; - return ( -
-
-

Export failed

-

- {error} -

-
+ return ( +
+
+

Export failed

+

{error}

+
-
- - -
-
- ); +
+ + +
+
+ ); } -- cgit v1.3.1