diff --git a/packages/studio/package.json b/packages/studio/package.json index d8be6bb863..ad34b16899 100644 --- a/packages/studio/package.json +++ b/packages/studio/package.json @@ -45,7 +45,7 @@ "types": "./dist/index.d.ts" }, "scripts": { - "dev": "vite", + "dev": "bun --bun ./node_modules/.bin/vite --host 127.0.0.1", "build": "vite build && tsup", "typecheck": "tsc --noEmit", "test": "vitest run", diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index b29c5213a4..6b23e67524 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -38,7 +38,6 @@ import { useToast } from "./hooks/useToast"; import { useCompositionContentLoader } from "./hooks/useCompositionContentLoader"; import { useStudioUrlState } from "./hooks/useStudioUrlState"; import { useEffectiveTimelineDuration } from "./hooks/useEffectiveTimelineDuration"; -import { useAudioSoloBridge } from "./hooks/useAudioSoloBridge"; import { buildStudioContextValue, useGlobalFileDrop, @@ -82,7 +81,6 @@ export function StudioApp() { const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion(); const [blockPreview, setBlockPreview] = useState(null); const previewIframeRef = useRef(null); - useAudioSoloBridge(previewIframeRef); const activeCompPathRef = useRef(activeCompPath); activeCompPathRef.current = activeCompPath; const leftSidebarRef = useRef(null); diff --git a/packages/studio/src/components/StudioRightPanel.tsx b/packages/studio/src/components/StudioRightPanel.tsx index 9b37ee46c2..e559af8c80 100644 --- a/packages/studio/src/components/StudioRightPanel.tsx +++ b/packages/studio/src/components/StudioRightPanel.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; +import { useCallback } from "react"; import type { StudioRightPanelProps } from "./StudioRightPanel.types"; export type { StudioRightPanelProps }; @@ -20,15 +20,15 @@ import { usePanelLayoutContext } from "../contexts/PanelLayoutContext"; import { useFileManagerContext } from "../contexts/FileManagerContext"; import { useDomEditContext } from "../contexts/DomEditContext"; import { usePlayerStore } from "../player"; -import { waitForMediaJob } from "./studioMediaJobs"; import { applyColorGradingScopeUpdate, EMPTY_COLOR_GRADING_SCOPE_RESULT, type ColorGradingScope, } from "./studioColorGradingScope"; -import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes"; import { timelineKeysForSelections } from "../utils/studioHelpers"; +import { canHideSelections } from "../utils/timelineInspector"; import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize"; +import { useRemoveBackground } from "../hooks/useRemoveBackground"; // fallow-ignore-next-line complexity export function StudioRightPanel({ @@ -164,14 +164,6 @@ export function StudioRightPanel({ handleInspectorSplitResizeMove, handleInspectorSplitResizeEnd, } = useInspectorSplitResize(); - const backgroundRemovalAbortRef = useRef(null); - - useEffect( - () => () => { - backgroundRemovalAbortRef.current?.abort(); - }, - [], - ); const renderJobs = renderQueue.jobs as RenderJob[]; const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers"; @@ -238,52 +230,7 @@ export function StudioRightPanel({ ], ); - const handleRemoveBackground = useCallback( - // fallow-ignore-next-line complexity - async ( - inputPath: string, - options: { - createBackgroundPlate?: boolean; - quality?: "fast" | "balanced" | "best"; - onProgress?: (progress: BackgroundRemovalProgress) => void; - }, - ) => { - const response = await fetch( - `/api/projects/${encodeURIComponent(projectId)}/media/remove-background`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - inputPath, - createBackgroundPlate: options.createBackgroundPlate === true, - quality: options.quality ?? "balanced", - }), - }, - ); - const data = (await response.json().catch(() => ({}))) as { - jobId?: string; - error?: string; - }; - if (!response.ok || !data.jobId) { - throw new Error(data.error || `Background removal failed (${response.status})`); - } - showToast("Removing background...", "info"); - backgroundRemovalAbortRef.current?.abort(); - const controller = new AbortController(); - backgroundRemovalAbortRef.current = controller; - try { - const result = await waitForMediaJob(data.jobId, options.onProgress, controller.signal); - await refreshFileTree(); - showToast(`Created transparent asset: ${result.outputPath.split("/").pop()}`, "info"); - return result; - } finally { - if (backgroundRemovalAbortRef.current === controller) { - backgroundRemovalAbortRef.current = null; - } - } - }, - [projectId, refreshFileTree, showToast], - ); + const handleRemoveBackground = useRemoveBackground(projectId, refreshFileTree, showToast); /** * A dial being dragged writes to the preview and stops there. @@ -300,6 +247,17 @@ export function StudioRightPanel({ [handleDomAttributeLiveCommit], ); const handleHideAllSelected = () => { + // Audio has no visual to hide, and `data-hidden` on an audio element is what + // MUTES it — preview silences it and the render drops it from the mix. The + // timeline withholds the eye on an audio track for that reason + // (`visible={!isAudioTrack}`), and the single-selection panel gates the same + // write on `audioSelection`; this multi-selection path was the way back to + // it. Checked here as well as in the panel because the button is not the + // only caller. + if (!canHideSelections(domEditGroupSelections)) { + showToast("Audio can't be hidden — use the group's own controls", "info"); + return; + } const { elements } = usePlayerStore.getState(); const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath); if (keys.length > 0) void onToggleElementHidden?.(keys, true); diff --git a/packages/studio/src/components/editor/PropertyPanel.test.tsx b/packages/studio/src/components/editor/PropertyPanel.test.tsx index 34b691fe93..9d47b6ce57 100644 --- a/packages/studio/src/components/editor/PropertyPanel.test.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.test.tsx @@ -22,7 +22,7 @@ afterEach(() => { vi.resetModules(); }); -function baseElement() { +function baseElement(): NonNullable { return { element: document.createElement("div"), id: "mono-label", @@ -81,7 +81,7 @@ function nonTextElement() { // flat multi-field layer list (FlatTextLayerList + FlatTextFieldEditor) — // must not double-render the "Text" heading (FlatGroup's own heading; this // component never renders one of its own). -function multiFieldTextElement() { +function multiFieldTextElement(): NonNullable { const base = baseElement(); return { ...base, @@ -177,6 +177,43 @@ function sixGroupElement() { }; } +/** An `