import { useEffect, useState } from "react"; import { Check, ClipboardList, Film, Music, Scissors } from "../../icons/SystemIcons"; import type { DomEditSelection } from "./domEditing"; import { type BackgroundRemovalProgress, type BackgroundRemovalResult, formatNumericValue, formatTimingValue, LABEL, parseNumericValue, RESPONSIVE_GRID, stripQueryAndHash, } from "./propertyPanelHelpers"; import { Section, SegmentedControl, SelectField, SliderControl } from "./propertyPanelPrimitives"; // fallow-ignore-next-line complexity export function MediaSection({ projectDir, element, styles, onSetStyle, onSetAttribute, onSetHtmlAttribute, onRemoveBackground, }: { projectDir: string | null; element: DomEditSelection; styles: Record; onSetStyle: (prop: string, value: string) => void | Promise; onSetAttribute: (attr: string, value: string) => void | Promise; onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise; onRemoveBackground?: ( inputPath: string, options: { createBackgroundPlate?: boolean; quality?: "fast" | "balanced" | "best"; onProgress?: (progress: BackgroundRemovalProgress) => void; }, ) => Promise; }) { const isVideo = element.tagName === "video"; const isAudio = element.tagName === "audio"; const isImage = element.tagName === "img"; const isVisualMedia = isVideo || isImage; const el = element.element; const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1; const volumePercent = Math.round(volume * 100); const mediaStart = Number.parseFloat( element.dataAttributes["media-start"] ?? element.dataAttributes["playback-start"] ?? "0", ) || 0; const hasLoop = el.hasAttribute("loop"); const hasMuted = el.hasAttribute("muted"); const hasAudio = element.dataAttributes["has-audio"] === "true"; const playbackRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1; const objectFit = styles["object-fit"] || "contain"; const objectPosition = styles["object-position"] || "center"; const sourceDuration = Number.parseFloat(element.dataAttributes["source-duration"] ?? "") || (el as HTMLMediaElement).duration || 0; const mediaStartMax = Math.max(30, Math.ceil(sourceDuration || mediaStart + 10)); const srcAttr = el.getAttribute("src") ?? ""; const [copied, setCopied] = useState(false); const [removeBusy, setRemoveBusy] = useState(false); const [removeProgress, setRemoveProgress] = useState(null); const [createPlate, setCreatePlate] = useState(false); const [quality, setQuality] = useState<"fast" | "balanced" | "best">("balanced"); const absoluteSrc = projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr; const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : ""; const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc); const panelTitle = isImage ? "Image" : isVideo ? "Video" : "Audio"; useEffect(() => { setRemoveProgress(null); setCreatePlate(false); }, [srcAttr]); const applyCutoutResult = async (result: BackgroundRemovalResult) => { await onSetHtmlAttribute("src", result.outputPath); if (isVideo) { await onSetAttribute("has-audio", ""); await onSetHtmlAttribute("muted", "true"); } }; const runBackgroundRemoval = async () => { if (!onRemoveBackground || !projectSrc || removeBusy) return; setRemoveBusy(true); setRemoveProgress({ status: "processing", progress: 0, stage: "Preparing" }); try { const result = await onRemoveBackground(projectSrc, { createBackgroundPlate: isVideo && createPlate, quality, onProgress: setRemoveProgress, }); await applyCutoutResult(result); setRemoveProgress({ status: "complete", progress: 100, stage: "Applied cutout", ...result, }); } catch (error) { setRemoveProgress({ status: "failed", progress: 0, stage: "Failed", error: error instanceof Error ? error.message : String(error), }); } finally { setRemoveBusy(false); } }; return (
: }>
{srcAttr && (
Source
{absoluteSrc}
)} {isVisualMedia && (
Cutout
Create transparent {isVideo ? "WebM video" : "PNG image"}
setQuality(next as typeof quality)} options={["fast", "balanced", "best"]} /> {isVideo ? (
BG plate setCreatePlate(next === "on")} options={[ { label: "On", value: "on" }, { label: "Off", value: "off" }, ]} /> Optional hole-cut background copy.
) : (
)}
{removeProgress && (
{removeProgress.error ?? removeProgress.stage ?? "Processing"} {Math.round(removeProgress.progress)}%
)} {removeProgress?.status === "complete" && removeProgress.outputPath && (
Applied {removeProgress.outputPath}
)}
)} {(isVideo || isAudio) && ( <>
Volume `${Math.round(next)}%`} onCommit={(next) => { void onSetAttribute("volume", formatNumericValue(next / 100)); }} />
Playback rate `${formatNumericValue(next / 100)}x`} onCommit={(next) => { void onSetAttribute("playback-rate", formatNumericValue(next / 100)); }} />
Media start formatTimingValue(next / 100)} onCommit={(next) => { void onSetAttribute("media-start", (next / 100).toFixed(2)); }} />
Loop { void onSetHtmlAttribute("loop", next === "on" ? "true" : null); }} options={[ { label: "On", value: "on" }, { label: "Off", value: "off" }, ]} />
Muted { void onSetHtmlAttribute("muted", next === "on" ? "true" : null); }} options={[ { label: "On", value: "on" }, { label: "Off", value: "off" }, ]} />
{isVideo && (
Has audio track { if (next === "yes") { void onSetAttribute("has-audio", "true"); void onSetHtmlAttribute("muted", null); } else { void onSetAttribute("has-audio", ""); void onSetHtmlAttribute("muted", "true"); } }} options={[ { label: "Yes", value: "yes" }, { label: "No", value: "no" }, ]} />
)} )} {isVisualMedia && ( <>
{ void onSetStyle("object-fit", next); }} options={["contain", "cover", "fill", "none", "scale-down"]} /> { void onSetStyle("object-position", next); }} options={[ "center", "top", "bottom", "left", "right", "left top", "right top", "left bottom", "right bottom", ]} />
)}
); }