@hyperframes/studio 0.8.114 → 0.8.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
- package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
- package/dist/index.d.ts +146 -40
- package/dist/index.html +2 -2
- package/dist/index.js +4021 -3591
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +2 -2
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.ts +4 -3
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +41 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -2
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +5 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +4 -125
- package/src/hooks/gsapDragPositionCommit.ts +36 -384
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +96 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -168
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +13 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +8 -6
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
- package/src/hooks/useDomEditTextCommits.ts +13 -3
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +8 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +34 -3
- package/src/hooks/useEnableKeyframes.ts +7 -4
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.ts +3 -2
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +101 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
- package/src/hooks/useGsapScriptCommits.ts +15 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +22 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/sceneSwap.ts +2 -1
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +51 -5
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +4 -3
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -6,11 +6,22 @@ import {
|
|
|
6
6
|
StudioSaveNetworkError,
|
|
7
7
|
} from "../utils/studioSaveDiagnostics";
|
|
8
8
|
import { studioExpectedFileVersion, studioWriteHeaders } from "../utils/studioFileVersion";
|
|
9
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
9
10
|
|
|
10
11
|
export interface UseProjectFileWriterOptions {
|
|
11
12
|
projectId: string | null;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
async function fetchProjectFile(projectId: string | null, path: string, query: string) {
|
|
16
|
+
if (!projectId) throw new Error("No active project");
|
|
17
|
+
const response = await studioApiFetch(
|
|
18
|
+
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}${query}`,
|
|
19
|
+
);
|
|
20
|
+
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
21
|
+
const data = (await response.json()) as { content?: string; version?: string };
|
|
22
|
+
return { content: data.content, version: data.version ?? response.headers.get("etag") };
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
// The etag-guarded project-file read/write pair every Studio save path
|
|
15
26
|
// shares. A host mounting hand editing outside Studio needs only this, not
|
|
16
27
|
// the full file manager (file tree, editor tabs, uploads, font assets).
|
|
@@ -30,29 +41,19 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
|
|
|
30
41
|
|
|
31
42
|
const readProjectFile = useCallback(
|
|
32
43
|
async (path: string): Promise<string> => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
38
|
-
const data = (await response.json()) as { content?: string; version?: string };
|
|
39
|
-
if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
|
|
40
|
-
fileVersions.set(path, data.version ?? response.headers.get("etag"));
|
|
41
|
-
return data.content;
|
|
44
|
+
const { content, version } = await fetchProjectFile(projectId, path, "");
|
|
45
|
+
if (typeof content !== "string") throw new Error(`Missing file contents for ${path}`);
|
|
46
|
+
fileVersions.set(path, version);
|
|
47
|
+
return content;
|
|
42
48
|
},
|
|
43
49
|
[fileVersions, projectId],
|
|
44
50
|
);
|
|
45
51
|
|
|
46
52
|
const readOptionalProjectFile = useCallback(
|
|
47
53
|
async (path: string): Promise<string> => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
52
|
-
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
53
|
-
const data = (await response.json()) as { content?: string; version?: string };
|
|
54
|
-
fileVersions.set(path, data.version ?? response.headers.get("etag"));
|
|
55
|
-
return typeof data.content === "string" ? data.content : "";
|
|
54
|
+
const { content, version } = await fetchProjectFile(projectId, path, "?optional=1");
|
|
55
|
+
fileVersions.set(path, version);
|
|
56
|
+
return typeof content === "string" ? content : "";
|
|
56
57
|
},
|
|
57
58
|
[fileVersions, projectId],
|
|
58
59
|
);
|
|
@@ -63,7 +64,7 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
|
|
|
63
64
|
const writeProjectId = projectId;
|
|
64
65
|
let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
|
|
65
66
|
if (expectedVersion === undefined) {
|
|
66
|
-
const preflight = await
|
|
67
|
+
const preflight = await studioApiFetch(
|
|
67
68
|
`/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
|
|
68
69
|
);
|
|
69
70
|
if (preflight.ok) {
|
|
@@ -86,7 +87,7 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
|
|
|
86
87
|
// the retry can produce a second filesystem receipt that must be suppressed independently.
|
|
87
88
|
let response: Response;
|
|
88
89
|
try {
|
|
89
|
-
response = await
|
|
90
|
+
response = await studioApiFetch(
|
|
90
91
|
`/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
|
|
91
92
|
{
|
|
92
93
|
method: "PUT",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { waitForMediaJob } from "../components/studioMediaJobs";
|
|
3
3
|
import type { BackgroundRemovalProgress } from "../components/editor/propertyPanelTypes";
|
|
4
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
4
5
|
|
|
5
6
|
interface RemoveBackgroundOptions {
|
|
6
7
|
createBackgroundPlate?: boolean;
|
|
@@ -31,7 +32,7 @@ export function useRemoveBackground(
|
|
|
31
32
|
return useCallback(
|
|
32
33
|
// fallow-ignore-next-line complexity
|
|
33
34
|
async (inputPath: string, options: RemoveBackgroundOptions) => {
|
|
34
|
-
const response = await
|
|
35
|
+
const response = await studioApiFetch(
|
|
35
36
|
`/api/projects/${encodeURIComponent(projectId)}/media/remove-background`,
|
|
36
37
|
{
|
|
37
38
|
method: "POST",
|
|
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
6
6
|
import { CompositionThumbnail, VideoThumbnail } from "../player";
|
|
7
7
|
import { AudioWaveform } from "../player/components/AudioWaveform";
|
|
8
8
|
import { AudibleVideoClipContent } from "../player/components/AudibleVideoClipContent";
|
|
9
|
+
import { TextClipContent } from "../player/components/TextClipContent";
|
|
9
10
|
import { ClipPeakMarks } from "../player/components/ClipPeakMarks";
|
|
10
11
|
|
|
11
12
|
function unwrapPeakMarks(node: ReactNode): ReactNode {
|
|
@@ -280,6 +281,23 @@ describe("useRenderClipContent", () => {
|
|
|
280
281
|
expect(isValidElement(content) && content.type).toBe(AudibleVideoClipContent);
|
|
281
282
|
});
|
|
282
283
|
|
|
284
|
+
it("gives a layer spanning the whole film its own frame, whatever its id", () => {
|
|
285
|
+
usePlayerStore.setState({ thumbnailMode: "adaptive" });
|
|
286
|
+
for (const id of ["waves", "background-glyphs"]) {
|
|
287
|
+
const content = renderClipContent({ id, tag: "div", start: 0, duration: 12, track: 0 }, null);
|
|
288
|
+
expect(isValidElement(content) && content.type).toBe(CompositionThumbnail);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("draws a text layer's own words instead of capturing them", () => {
|
|
293
|
+
usePlayerStore.setState({ thumbnailMode: "adaptive" });
|
|
294
|
+
const content = renderClipContent(
|
|
295
|
+
{ id: "title", tag: "h1", start: 0, duration: 12, track: 0, text: { value: "Ship it" } },
|
|
296
|
+
null,
|
|
297
|
+
);
|
|
298
|
+
expect(isValidElement(content) && content.type).toBe(TextClipContent);
|
|
299
|
+
});
|
|
300
|
+
|
|
283
301
|
it("passes empty labels to thumbnail content so TimelineClip owns clip names", () => {
|
|
284
302
|
usePlayerStore.setState({ thumbnailMode: "adaptive" });
|
|
285
303
|
|
|
@@ -12,6 +12,7 @@ import type { TimelineClipRenderContext } from "../player/components/TimelineTyp
|
|
|
12
12
|
import { audioPillFlags } from "../player/components/audioClipLink";
|
|
13
13
|
import { AudioWaveform, rendersWaveform } from "../player/components/AudioWaveform";
|
|
14
14
|
import { ImageThumbnail } from "../player/components/ImageThumbnail";
|
|
15
|
+
import { TextClipContent } from "../player/components/TextClipContent";
|
|
15
16
|
import { AudibleVideoClipContent } from "../player/components/AudibleVideoClipContent";
|
|
16
17
|
import { ClipPeakMarks } from "../player/components/ClipPeakMarks";
|
|
17
18
|
import { clipPeaksUrl, clipSourceWindow } from "../player/components/clipPeakMap";
|
|
@@ -184,6 +185,8 @@ export function useRenderClipContent({
|
|
|
184
185
|
return renderAudioClip(el, pid, sessionEpoch, style.label, context, elements);
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
if (el.text) return createElement(TextClipContent, { text: el.text });
|
|
189
|
+
|
|
187
190
|
// When drilled into a composition, render all inner elements via
|
|
188
191
|
// CompositionThumbnail at their start time — most accurate visual.
|
|
189
192
|
if (activePreviewUrl && el.duration > 0) {
|
|
@@ -207,11 +210,7 @@ export function useRenderClipContent({
|
|
|
207
210
|
});
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
const htmlPreviewEligible =
|
|
211
|
-
el.duration > 0 &&
|
|
212
|
-
effectiveTimelineDuration > 0 &&
|
|
213
|
-
el.duration < effectiveTimelineDuration * 0.92 &&
|
|
214
|
-
!/(backdrop|background|overlay|scrim|mask)/i.test(el.id);
|
|
213
|
+
const htmlPreviewEligible = el.duration > 0 && effectiveTimelineDuration > 0;
|
|
215
214
|
|
|
216
215
|
if ((el.tag === "video" || el.tag === "img") && el.src) {
|
|
217
216
|
const mediaSrc = resolveMediaPreviewUrl(el.src, pid, window.location.origin);
|
|
@@ -6,6 +6,7 @@ import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
|
6
6
|
import type { PublishSdkSession } from "../utils/sdkCutover";
|
|
7
7
|
import { addExternalFileReloadListener } from "./externalFileReloadBus";
|
|
8
8
|
import { whenPreviewBooted } from "../player/store/playerStore";
|
|
9
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Why an optional project-file read produced no usable content. `stage: "read"`
|
|
@@ -194,7 +195,7 @@ async function readProjectFileOptional(
|
|
|
194
195
|
let res: Response;
|
|
195
196
|
const fetchStarted = performance.now();
|
|
196
197
|
try {
|
|
197
|
-
res = await
|
|
198
|
+
res = await studioApiFetch(
|
|
198
199
|
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`,
|
|
199
200
|
);
|
|
200
201
|
} catch {
|
|
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|
|
2
2
|
import { buildProjectHash, parseProjectIdFromHash } from "../utils/projectRouting";
|
|
3
3
|
import { useMountEffect } from "./useMountEffect";
|
|
4
4
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
5
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
5
6
|
|
|
6
7
|
interface ServerConnectionState {
|
|
7
8
|
projectId: string | null;
|
|
@@ -37,7 +38,7 @@ type ProjectHashVerdict = {
|
|
|
37
38
|
|
|
38
39
|
async function resolveHashProject(id: string): Promise<ProjectHashVerdict> {
|
|
39
40
|
try {
|
|
40
|
-
const res = await
|
|
41
|
+
const res = await studioApiFetch(`/api/projects/${encodeURIComponent(id)}`);
|
|
41
42
|
if (res.ok) return { outcome: "ok", status: res.status };
|
|
42
43
|
return { outcome: res.status === 404 ? "missing" : "unknown", status: res.status };
|
|
43
44
|
} catch {
|
|
@@ -72,7 +73,7 @@ function replaceProjectHash(projectId: string): void {
|
|
|
72
73
|
*/
|
|
73
74
|
async function firstProjectId(): Promise<string | null> {
|
|
74
75
|
try {
|
|
75
|
-
const res = await
|
|
76
|
+
const res = await studioApiFetch("/api/projects");
|
|
76
77
|
const data = (await res.json()) as { projects?: Array<{ id?: string }> };
|
|
77
78
|
return data.projects?.[0]?.id ?? null;
|
|
78
79
|
} catch {
|
|
@@ -107,7 +108,7 @@ export function useServerConnection(): ServerConnectionState {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function tryConnect() {
|
|
110
|
-
|
|
111
|
+
studioApiFetch("/api/projects")
|
|
111
112
|
.then((r) => r.json())
|
|
112
113
|
.then(async (data) => {
|
|
113
114
|
if (cancelled) return;
|
|
@@ -3,6 +3,7 @@ import type { DomEditSelection } from "../components/editor/domEditing";
|
|
|
3
3
|
import { usePlayerStore } from "../player";
|
|
4
4
|
import { buildStudioSelectionSnapshot } from "../utils/studioSelectionSnapshot";
|
|
5
5
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
6
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
6
7
|
|
|
7
8
|
interface UseStudioSelectionPublisherParams {
|
|
8
9
|
projectId: string | null;
|
|
@@ -26,7 +27,7 @@ function reportSelectionPublishError(error: unknown): void {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function putSelection(projectId: string, selection: unknown, signal?: AbortSignal): Promise<void> {
|
|
29
|
-
return
|
|
30
|
+
return studioApiFetch(`/api/projects/${encodeURIComponent(projectId)}/selection`, {
|
|
30
31
|
method: "PUT",
|
|
31
32
|
headers: { "Content-Type": "application/json" },
|
|
32
33
|
body: JSON.stringify({ selection }),
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { thumbnailScheduler } from "../player/lib/thumbnailScheduler";
|
|
7
|
+
// By package name, as a host imports it: the test fails if the export goes.
|
|
8
|
+
import { useThumbnailStill, type ThumbnailStill } from "@hyperframes/studio";
|
|
9
|
+
|
|
10
|
+
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", { configurable: true, value: true });
|
|
11
|
+
|
|
12
|
+
class MockImage {
|
|
13
|
+
onload: (() => void) | null = null;
|
|
14
|
+
onerror: (() => void) | null = null;
|
|
15
|
+
naturalWidth = 320;
|
|
16
|
+
naturalHeight = 180;
|
|
17
|
+
set src(_value: string) {
|
|
18
|
+
queueMicrotask(() => this.onload?.());
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const URL_ONE = "http://localhost:3000/api/projects/demo/thumbnail/index.html?t=1.00&revision=1";
|
|
23
|
+
const originalImage = globalThis.Image;
|
|
24
|
+
const originalFetch = globalThis.fetch;
|
|
25
|
+
const originalCreate = URL.createObjectURL;
|
|
26
|
+
const originalRevoke = URL.revokeObjectURL;
|
|
27
|
+
let host: HTMLDivElement;
|
|
28
|
+
let root: Root | null = null;
|
|
29
|
+
let shown: ThumbnailStill | null = null;
|
|
30
|
+
|
|
31
|
+
function Still({ url }: { url: string | null }) {
|
|
32
|
+
shown = useThumbnailStill(url, "demo");
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const mount = (url: string | null) =>
|
|
37
|
+
act(async () => {
|
|
38
|
+
root ??= createRoot(host);
|
|
39
|
+
root.render(React.createElement(Still, { url }));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
globalThis.Image = MockImage as unknown as typeof Image;
|
|
44
|
+
globalThis.fetch = vi.fn(async () => new Response(new Blob(["still"]), { status: 200 }));
|
|
45
|
+
URL.createObjectURL = vi.fn(() => "blob:still");
|
|
46
|
+
URL.revokeObjectURL = vi.fn();
|
|
47
|
+
shown = null;
|
|
48
|
+
host = document.createElement("div");
|
|
49
|
+
document.body.append(host);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
act(() => root?.unmount());
|
|
54
|
+
root = null;
|
|
55
|
+
thumbnailScheduler.setPreviewReloading(false);
|
|
56
|
+
thumbnailScheduler.invalidateProject("demo");
|
|
57
|
+
globalThis.Image = originalImage;
|
|
58
|
+
globalThis.fetch = originalFetch;
|
|
59
|
+
URL.createObjectURL = originalCreate;
|
|
60
|
+
URL.revokeObjectURL = originalRevoke;
|
|
61
|
+
document.body.replaceChildren();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("useThumbnailStill", () => {
|
|
65
|
+
it("hands back the still's object URL once the scheduler has loaded it", async () => {
|
|
66
|
+
await mount(URL_ONE);
|
|
67
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(URL_ONE, expect.anything());
|
|
68
|
+
await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("hands back the same object while nothing changes", async () => {
|
|
72
|
+
await mount(URL_ONE);
|
|
73
|
+
await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
|
|
74
|
+
const first = shown;
|
|
75
|
+
await mount(URL_ONE);
|
|
76
|
+
expect(shown).toBe(first);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("asks for nothing without a url", async () => {
|
|
80
|
+
await mount(null);
|
|
81
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
82
|
+
expect(shown).toEqual({ status: "loading" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("says failed when the still cannot load, so a host can fall back", async () => {
|
|
86
|
+
globalThis.fetch = vi.fn(async () => new Response("", { status: 500 }));
|
|
87
|
+
await mount(URL_ONE);
|
|
88
|
+
await vi.waitFor(() => expect(shown).toEqual({ status: "failed" }));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("holds the fetch while a preview reloads, and runs it once the reload is over", async () => {
|
|
92
|
+
thumbnailScheduler.setPreviewReloading(true);
|
|
93
|
+
await mount(URL_ONE);
|
|
94
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
95
|
+
expect(shown).toEqual({ status: "loading" });
|
|
96
|
+
await act(async () => thumbnailScheduler.setPreviewReloading(false));
|
|
97
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
98
|
+
await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { compositionThumbnailRequest } from "../player/components/CompositionThumbnail";
|
|
3
|
+
import type { ThumbnailPriority } from "../player/lib/thumbnailScheduler";
|
|
4
|
+
import { useThumbnailLease } from "./useThumbnailLease";
|
|
5
|
+
|
|
6
|
+
export interface UseThumbnailStillOptions {
|
|
7
|
+
sessionEpoch?: number;
|
|
8
|
+
priority?: ThumbnailPriority;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ThumbnailStill =
|
|
12
|
+
| { status: "loading" }
|
|
13
|
+
| { status: "ready"; url: string }
|
|
14
|
+
| { status: "failed" };
|
|
15
|
+
|
|
16
|
+
const LOADING: ThumbnailStill = Object.freeze({ status: "loading" });
|
|
17
|
+
const FAILED: ThumbnailStill = Object.freeze({ status: "failed" });
|
|
18
|
+
|
|
19
|
+
/** A host picture's server still through the thumbnail scheduler, so it yields to a preview reload. */
|
|
20
|
+
export function useThumbnailStill(
|
|
21
|
+
url: string | null,
|
|
22
|
+
projectId: string,
|
|
23
|
+
{ sessionEpoch = 0, priority = "visible" }: UseThumbnailStillOptions = {},
|
|
24
|
+
): ThumbnailStill {
|
|
25
|
+
const request = useMemo(
|
|
26
|
+
() => (url ? compositionThumbnailRequest(url, projectId, { sessionEpoch, priority }) : null),
|
|
27
|
+
[priority, projectId, sessionEpoch, url],
|
|
28
|
+
);
|
|
29
|
+
const snapshot = useThumbnailLease(request);
|
|
30
|
+
const ready =
|
|
31
|
+
snapshot.status === "ready" && snapshot.value.kind === "image" ? snapshot.value.url : null;
|
|
32
|
+
return useMemo(
|
|
33
|
+
() =>
|
|
34
|
+
ready ? { status: "ready", url: ready } : snapshot.status === "error" ? FAILED : LOADING,
|
|
35
|
+
[ready, snapshot.status],
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -28,6 +28,17 @@ describe("applyRippleShifts", () => {
|
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
function stubRemoveElementsFetch(current: string, afterRemoval: string) {
|
|
32
|
+
vi.stubGlobal(
|
|
33
|
+
"fetch",
|
|
34
|
+
vi.fn(async (input: RequestInfo | URL) => {
|
|
35
|
+
const isRemoval = String(input).includes("/file-mutations/remove-elements/");
|
|
36
|
+
const content = isRemoval ? afterRemoval : current;
|
|
37
|
+
return new Response(JSON.stringify({ changed: isRemoval, content }), { status: 200 });
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
describe("useTimelineDeleteOps: ripple undo label", () => {
|
|
32
43
|
const html = `<!DOCTYPE html><html data-composition-variables='[]'><body>
|
|
33
44
|
<div data-hf-id="hf-stage" data-hf-root data-duration="6">
|
|
@@ -38,16 +49,7 @@ describe("useTimelineDeleteOps: ripple undo label", () => {
|
|
|
38
49
|
</body></html>`;
|
|
39
50
|
|
|
40
51
|
beforeEach(() => {
|
|
41
|
-
|
|
42
|
-
"fetch",
|
|
43
|
-
vi.fn(async (input: RequestInfo | URL) => {
|
|
44
|
-
const url = String(input);
|
|
45
|
-
if (url.includes("/file-mutations/remove-element/")) {
|
|
46
|
-
return new Response(JSON.stringify({ changed: true, content: html }), { status: 200 });
|
|
47
|
-
}
|
|
48
|
-
return new Response(JSON.stringify({ content: html }), { status: 200 });
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
52
|
+
stubRemoveElementsFetch(html, html);
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
afterEach(() => {
|
|
@@ -118,6 +120,23 @@ describe("useTimelineDeleteOps: ripple undo label", () => {
|
|
|
118
120
|
);
|
|
119
121
|
});
|
|
120
122
|
|
|
123
|
+
it("removes a multi-clip selection with one request, however many clips it holds", async () => {
|
|
124
|
+
const handleTimelineGroupMove = vi.fn().mockResolvedValue(undefined);
|
|
125
|
+
const { elements, getHook } = mountDeleteHarness({ handleTimelineGroupMove });
|
|
126
|
+
|
|
127
|
+
await act(async () => {
|
|
128
|
+
await getHook().handleTimelineElementsDelete(elements);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const removals = vi
|
|
132
|
+
.mocked(fetch)
|
|
133
|
+
.mock.calls.filter(([url]) => String(url).includes("/file-mutations/remove-element"));
|
|
134
|
+
expect(removals).toHaveLength(1);
|
|
135
|
+
const [, init] = removals[0] as [unknown, RequestInit];
|
|
136
|
+
const body = JSON.parse(String(init.body));
|
|
137
|
+
expect(body.targets).toHaveLength(elements.length);
|
|
138
|
+
});
|
|
139
|
+
|
|
121
140
|
it("shows exactly one toast when a ripple fails to persist after a committed delete", async () => {
|
|
122
141
|
const handleTimelineGroupMove = vi.fn().mockRejectedValue(new Error("persist failed"));
|
|
123
142
|
const showToast = vi.fn();
|
|
@@ -162,18 +181,7 @@ describe("useTimelineDeleteOps: undo race", () => {
|
|
|
162
181
|
</body></html>`;
|
|
163
182
|
|
|
164
183
|
beforeEach(() => {
|
|
165
|
-
|
|
166
|
-
"fetch",
|
|
167
|
-
vi.fn(async (input: RequestInfo | URL) => {
|
|
168
|
-
const url = String(input);
|
|
169
|
-
if (url.includes("/file-mutations/remove-element/")) {
|
|
170
|
-
return new Response(JSON.stringify({ changed: true, content: htmlAfterDeletingC }), {
|
|
171
|
-
status: 200,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
return new Response(JSON.stringify({ content: html }), { status: 200 });
|
|
175
|
-
}),
|
|
176
|
-
);
|
|
184
|
+
stubRemoveElementsFetch(html, htmlAfterDeletingC);
|
|
177
185
|
});
|
|
178
186
|
|
|
179
187
|
afterEach(() => {
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
TimelineGroupCommitOptions,
|
|
22
22
|
TimelineGroupMoveChange,
|
|
23
23
|
} from "./useTimelineGroupEditing";
|
|
24
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
24
25
|
|
|
25
26
|
/** Apply already-resolved ripple changes to the surviving elements for the
|
|
26
27
|
* optimistic store update after a delete. Pure — no IO. */
|
|
@@ -121,38 +122,32 @@ export function useTimelineDeleteOps({
|
|
|
121
122
|
writeFile: writeProjectFile,
|
|
122
123
|
recordEdit,
|
|
123
124
|
rewrite: async (originalContent) => {
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// undo, rather than one per clip.
|
|
128
|
-
let removedContent = originalContent;
|
|
129
|
-
for (const target of sameFile) {
|
|
125
|
+
// One request removes every selected element and rewrites the file once, so a
|
|
126
|
+
// large selection is a single round trip and a single history entry.
|
|
127
|
+
const patchTargets = sameFile.map((target) => {
|
|
130
128
|
const patchTarget = buildPatchTarget(target);
|
|
131
129
|
if (!patchTarget) {
|
|
132
130
|
throw new Error(`Timeline element ${target.id} is missing a patchable target`);
|
|
133
131
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const removeData = (await removeResponse.json()) as {
|
|
151
|
-
changed?: boolean;
|
|
152
|
-
content?: string;
|
|
153
|
-
};
|
|
154
|
-
if (typeof removeData.content === "string") removedContent = removeData.content;
|
|
132
|
+
return patchTarget;
|
|
133
|
+
});
|
|
134
|
+
const removeResponse = await studioApiFetch(
|
|
135
|
+
buildProjectApiPath(
|
|
136
|
+
pid,
|
|
137
|
+
`/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
|
|
138
|
+
),
|
|
139
|
+
{
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
|
|
142
|
+
body: JSON.stringify({ targets: patchTargets }),
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
if (!removeResponse.ok) {
|
|
146
|
+
throw new Error(`Failed to delete ${sameFile.length} clips from ${targetPath}`);
|
|
155
147
|
}
|
|
148
|
+
const removeData = (await removeResponse.json()) as { content?: string };
|
|
149
|
+
const removedContent =
|
|
150
|
+
typeof removeData.content === "string" ? removeData.content : originalContent;
|
|
156
151
|
// Shrink to the furthest remaining clip end, read from the post-removal source:
|
|
157
152
|
// store durations are runtime-truncated.
|
|
158
153
|
const deleteContentEnd = furthestClipEndFromSource(removedContent);
|
|
@@ -1296,7 +1296,7 @@ describe("useTimelineEditing duration rollback on failed persist", () => {
|
|
|
1296
1296
|
if (url.includes("/api/projects/p1/files/")) {
|
|
1297
1297
|
return jsonResponse({ content: DELETE_SOURCE });
|
|
1298
1298
|
}
|
|
1299
|
-
if (url.includes("/api/projects/p1/file-mutations/remove-
|
|
1299
|
+
if (url.includes("/api/projects/p1/file-mutations/remove-elements/")) {
|
|
1300
1300
|
return jsonResponse({ changed: true, content: DELETE_REMOVED_SOURCE });
|
|
1301
1301
|
}
|
|
1302
1302
|
throw new Error(`Unexpected fetch: ${url}`);
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,8 @@ export type { AudioWaveformProps } from "./player/components/AudioWaveform";
|
|
|
76
76
|
export { ImageThumbnail } from "./player/components/ImageThumbnail";
|
|
77
77
|
export type { ImageThumbnailProps } from "./player/components/ImageThumbnail";
|
|
78
78
|
export { useRenderClipContent } from "./hooks/useRenderClipContent";
|
|
79
|
+
export { useThumbnailStill } from "./hooks/useThumbnailStill";
|
|
80
|
+
export type { ThumbnailStill, UseThumbnailStillOptions } from "./hooks/useThumbnailStill";
|
|
79
81
|
export type { UseRenderClipContentOptions } from "./hooks/useRenderClipContent";
|
|
80
82
|
export type { ThumbnailPriority } from "./player/lib/thumbnailScheduler";
|
|
81
83
|
export type {
|
|
@@ -208,6 +210,8 @@ export type { DomEditZOrder } from "./components/editor/useDomEditZOrder";
|
|
|
208
210
|
export type { ZOrderAction } from "./components/editor/canvasContextMenuZOrder";
|
|
209
211
|
export type { DomEditCapabilities, DomEditSelection } from "./components/editor/domEditingTypes";
|
|
210
212
|
export { useDomStyleCommit } from "./hooks/useDomStyleCommit";
|
|
213
|
+
export { ColorField } from "./components/editor/propertyPanelColor";
|
|
214
|
+
export { GradientField } from "./components/editor/propertyPanelFill";
|
|
211
215
|
export type { UseDomStyleCommitOptions } from "./hooks/useDomStyleCommit";
|
|
212
216
|
export type { DomEditCommitDeclineReason, DomEditCommitOutcome } from "./hooks/domEditCommitRunner";
|
|
213
217
|
export { resolveDomEditSelection } from "./components/editor/domEditingLayers";
|
|
@@ -5,6 +5,7 @@ import { useThumbnailLease } from "../../hooks/useThumbnailLease";
|
|
|
5
5
|
import { createThumbnailKey, type ThumbnailPriority } from "../lib/thumbnailScheduler";
|
|
6
6
|
import { decimatePeaks, loudnessToOpacity } from "./audioWaveformPeaks";
|
|
7
7
|
import { ClipFadesContext, type ClipFadeShape } from "./TimelineClipFades";
|
|
8
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
8
9
|
|
|
9
10
|
export interface AudioWaveformProps {
|
|
10
11
|
audioUrl: string;
|
|
@@ -124,7 +125,7 @@ async function loadWaveform(
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
async function fetchWaveformPeaks(url: string, signal: AbortSignal): Promise<number[]> {
|
|
127
|
-
const response = await
|
|
128
|
+
const response = await studioApiFetch(url, { signal });
|
|
128
129
|
if (!response.ok) throw new Error(`Waveform request failed (${response.status})`);
|
|
129
130
|
const data: unknown = await response.json();
|
|
130
131
|
if (
|
|
@@ -140,7 +141,7 @@ async function fetchWaveformPeaks(url: string, signal: AbortSignal): Promise<num
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
async function decodeWaveformPeaks(url: string, signal: AbortSignal): Promise<number[]> {
|
|
143
|
-
const response = await
|
|
144
|
+
const response = await studioApiFetch(url, { signal });
|
|
144
145
|
if (!response.ok) throw new Error(`Audio request failed (${response.status})`);
|
|
145
146
|
const buffer = await response.arrayBuffer();
|
|
146
147
|
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
@@ -8,6 +8,7 @@ import { memo } from "react";
|
|
|
8
8
|
import { createPortal } from "react-dom";
|
|
9
9
|
import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
|
|
10
10
|
import { AUTOMATION_SHAPES, type AutomationShapeId } from "./automationShapes";
|
|
11
|
+
import { menuClasses } from "../../components/ui/menuStyle";
|
|
11
12
|
|
|
12
13
|
interface AutomationSelectionMenuProps {
|
|
13
14
|
x: number;
|
|
@@ -45,7 +46,7 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
|
|
|
45
46
|
// portaled to `document.body`, but the ruler's sticky header sits at z-70
|
|
46
47
|
// in the SAME root stacking context, so a z-50 menu opened near the top of
|
|
47
48
|
// the timeline is painted through by the ruler and the playhead.
|
|
48
|
-
className=
|
|
49
|
+
className={`${menuClasses.panel} hf-automation-menu fixed z-200 min-w-[140px]`}
|
|
49
50
|
style={{ left: adjustedX, top: adjustedY }}
|
|
50
51
|
>
|
|
51
52
|
{AUTOMATION_SHAPES.map((shape) => (
|
|
@@ -61,7 +62,7 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
|
|
|
61
62
|
{shape.label}
|
|
62
63
|
</button>
|
|
63
64
|
))}
|
|
64
|
-
<div className=
|
|
65
|
+
<div className={menuClasses.divider} />
|
|
65
66
|
<button
|
|
66
67
|
type="button"
|
|
67
68
|
className={row}
|