@dailies-studio/shot-editor 0.1.0 → 0.2.0
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/.vite/manifest.json +382 -0
- package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
- package/dist/assets/AgentPanel-DJEphah0.css +1 -0
- package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
- package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
- package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
- package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
- package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
- package/dist/assets/CurvePane-BjFVabKX.css +1 -0
- package/dist/assets/CurvePane-R22WzllJ.js +1 -0
- package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
- package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
- package/dist/assets/FontPicker-v0IOIArM.css +1 -0
- package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
- package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
- package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
- package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
- package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
- package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
- package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
- package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
- package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
- package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
- package/dist/assets/SoundMode-BmcZru3i.js +3 -0
- package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
- package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
- package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
- package/dist/assets/board-B88EPuLO.css +1 -0
- package/dist/assets/board-CL-he7SL.js +3 -0
- package/dist/assets/components-0oOSPN0l.js +1 -0
- package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
- package/dist/assets/googleFonts-BNon6M7X.js +1 -0
- package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
- package/dist/assets/index-4F5NQmI8.css +1 -0
- package/dist/assets/index-BGHQGV5z.js +117 -0
- package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/assets/libraryModel-CpIwfFha.js +1 -0
- package/dist/assets/lucide-6iK64tTB.js +9319 -0
- package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
- package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
- package/dist/assets/webmcp-BsB9gxWC.js +2 -0
- package/dist/index.html +34 -3
- package/package.json +12 -9
- package/src/addFamilies.ts +66 -0
- package/src/api.ts +1631 -55
- package/src/app/App.tsx +977 -198
- package/src/app/lazyPanel.tsx +137 -0
- package/src/app/lazySurfaces.tsx +203 -0
- package/src/audio/index.tsx +41 -10
- package/src/audio/meter.ts +485 -0
- package/src/audio/meterMath.ts +273 -0
- package/src/audio/meterStore.ts +155 -0
- package/src/audio/offlineMix.ts +289 -0
- package/src/audio/preview.ts +928 -118
- package/src/bridge.ts +15 -5
- package/src/bridgeTypes.ts +1611 -36
- package/src/channels.ts +17 -32
- package/src/fonts/googleFonts.ts +4 -0
- package/src/hostApi.ts +174 -0
- package/src/icons.tsx +770 -0
- package/src/layerWords.ts +61 -0
- package/src/main.tsx +56 -4
- package/src/panels/ResizeHandle.tsx +33 -43
- package/src/panels/agent/AgentDots.tsx +36 -0
- package/src/panels/agent/AgentPanel.tsx +119 -43
- package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
- package/src/panels/agent/agent.css +113 -97
- package/src/panels/agent/agentGate.ts +35 -0
- package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
- package/src/panels/code/CodeStudio.tsx +373 -0
- package/src/panels/code/FileTree.tsx +265 -0
- package/src/panels/code/codeBufferGuard.ts +52 -0
- package/src/panels/code/codeParts.tsx +321 -0
- package/src/panels/comments/CommentComposer.tsx +239 -0
- package/src/panels/comments/CommentThread.tsx +346 -0
- package/src/panels/comments/CommentsPanel.tsx +336 -0
- package/src/panels/comments/InboxButton.tsx +131 -0
- package/src/panels/comments/MentionMenu.tsx +98 -0
- package/src/panels/comments/parts.tsx +171 -0
- package/src/panels/export/ExportDialog.tsx +919 -96
- package/src/panels/export/ExportScrim.tsx +57 -0
- package/src/panels/files/AssetsTab.tsx +656 -0
- package/src/panels/files/FilePanel.tsx +1466 -216
- package/src/panels/files/LayerMarks.tsx +40 -0
- package/src/panels/files/LeftColumn.tsx +37 -0
- package/src/panels/files/McpSection.tsx +690 -47
- package/src/panels/files/MissingAssetsNotice.tsx +60 -0
- package/src/panels/files/assetDrag.ts +79 -0
- package/src/panels/files/resourceParts.tsx +255 -0
- package/src/panels/fonts/FontPicker.tsx +762 -0
- package/src/panels/fonts/faceCache.ts +207 -0
- package/src/panels/fonts/fontPicker.css +271 -0
- package/src/panels/fonts/pickerModel.ts +265 -0
- package/src/panels/fonts/pickerRequest.ts +46 -0
- package/src/panels/header/AccountAvatar.tsx +76 -0
- package/src/panels/header/AgentChip.tsx +77 -0
- package/src/panels/header/AppMenu.tsx +213 -0
- package/src/panels/header/Header.tsx +564 -84
- package/src/panels/header/HistoryControls.tsx +31 -5
- package/src/panels/header/PresenceStack.tsx +329 -0
- package/src/panels/header/ProjectTitle.tsx +232 -0
- package/src/panels/header/ShareButton.tsx +59 -0
- package/src/panels/header/StatusBadges.tsx +358 -27
- package/src/panels/header/SyncDeletedDialog.tsx +88 -0
- package/src/panels/header/ZoomControls.tsx +46 -15
- package/src/panels/header/agentHandoff.ts +52 -0
- package/src/panels/header/leave.ts +26 -0
- package/src/panels/help/HelpSection.tsx +189 -163
- package/src/panels/insert/InsertDrawer.tsx +723 -0
- package/src/panels/insert/iconSet.ts +109 -0
- package/src/panels/inspector/ExposeDot.tsx +117 -0
- package/src/panels/inspector/FramingRows.tsx +314 -44
- package/src/panels/inspector/InspectorFooter.tsx +91 -0
- package/src/panels/inspector/InspectorHead.tsx +425 -0
- package/src/panels/inspector/InspectorPanel.tsx +408 -124
- package/src/panels/inspector/KeyframeDot.tsx +373 -0
- package/src/panels/inspector/KeyframeSection.tsx +257 -0
- package/src/panels/inspector/MarkerSection.tsx +55 -11
- package/src/panels/inspector/ParamRow.tsx +84 -53
- package/src/panels/inspector/RightColumn.tsx +152 -0
- package/src/panels/inspector/TimeSection.tsx +866 -0
- package/src/panels/inspector/animate/EaseControl.tsx +244 -0
- package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
- package/src/panels/inspector/animate/ShotSection.tsx +562 -48
- package/src/panels/inspector/design/AlignRow.tsx +95 -31
- package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
- package/src/panels/inspector/design/ClonerSection.tsx +323 -62
- package/src/panels/inspector/design/ComponentSection.tsx +50 -43
- package/src/panels/inspector/design/CornerSection.tsx +45 -14
- package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
- package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
- package/src/panels/inspector/design/DeviceSection.tsx +210 -0
- package/src/panels/inspector/design/FillSection.tsx +83 -51
- package/src/panels/inspector/design/FontField.tsx +196 -0
- package/src/panels/inspector/design/HtmlSection.tsx +23 -12
- package/src/panels/inspector/design/InstanceSection.tsx +373 -154
- package/src/panels/inspector/design/MaskSection.tsx +84 -0
- package/src/panels/inspector/design/MaterialSection.tsx +177 -0
- package/src/panels/inspector/design/MediaSection.tsx +273 -120
- package/src/panels/inspector/design/MeshSection.tsx +910 -82
- package/src/panels/inspector/design/PaintRows.tsx +550 -0
- package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
- package/src/panels/inspector/design/ShadowSection.tsx +221 -48
- package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
- package/src/panels/inspector/design/StrokeSection.tsx +228 -43
- package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
- package/src/panels/inspector/design/TransformSection.tsx +244 -50
- package/src/panels/inspector/design/TypographySection.tsx +679 -110
- package/src/panels/inspector/layout.ts +295 -0
- package/src/panels/inspector/propValue.ts +199 -0
- package/src/panels/inspector/scene/CameraSection.tsx +232 -22
- package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
- package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
- package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
- package/src/panels/inspector/scene/SceneSection.tsx +219 -44
- package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
- package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
- package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
- package/src/panels/inspector/timeModel.ts +190 -0
- package/src/panels/inspector/useDocWrite.ts +438 -146
- package/src/panels/library/LibraryBrowser.tsx +435 -0
- package/src/panels/library/LibraryGrid.tsx +190 -0
- package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
- package/src/panels/library/LibraryRail.tsx +84 -0
- package/src/panels/library/LibrarySoundPanel.tsx +293 -0
- package/src/panels/library/SoundHistory.tsx +245 -0
- package/src/panels/library/StockSounds.tsx +390 -0
- package/src/panels/library/libraryModel.ts +147 -0
- package/src/panels/library/stockSearch.ts +77 -0
- package/src/panels/maskMarks.tsx +117 -0
- package/src/panels/material/MaterialCommentNode.tsx +54 -0
- package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
- package/src/panels/material/MaterialGraphMenu.tsx +45 -0
- package/src/panels/material/MaterialGroupNode.tsx +56 -0
- package/src/panels/material/MaterialLibrary.tsx +253 -0
- package/src/panels/material/MaterialNode.tsx +453 -0
- package/src/panels/material/MaterialNodePalette.tsx +349 -0
- package/src/panels/material/MaterialParameterControls.tsx +525 -0
- package/src/panels/material/MaterialRerouteNode.tsx +42 -0
- package/src/panels/material/MaterialWorkspace.tsx +501 -0
- package/src/panels/material/NodePreview.tsx +70 -0
- package/src/panels/material/flowModel.ts +441 -0
- package/src/panels/material/parameterValues.ts +231 -0
- package/src/panels/material/previewScheduler.ts +235 -0
- package/src/panels/material/reactFlow.css +6 -0
- package/src/panels/material/socketGlyph.ts +85 -0
- package/src/panels/material/viewportMemory.ts +58 -0
- package/src/panels/preferences/PreferencesDialog.tsx +215 -0
- package/src/panels/share/ShareComponentDialog.tsx +247 -0
- package/src/panels/stage/StageMenu.tsx +35 -5
- package/src/panels/stage/StagePanel.tsx +338 -23
- package/src/panels/stage/cellSlots.tsx +70 -0
- package/src/panels/storyboard/BoardCanvas.tsx +739 -0
- package/src/panels/storyboard/BoardCursors.tsx +158 -0
- package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
- package/src/panels/storyboard/BoardPlan.tsx +42 -0
- package/src/panels/storyboard/ComponentSheet.tsx +187 -0
- package/src/panels/storyboard/ComponentStage.tsx +235 -0
- package/src/panels/storyboard/MiniSketch.tsx +173 -0
- package/src/panels/storyboard/PartPicker.tsx +64 -0
- package/src/panels/storyboard/PlanPanel.tsx +617 -0
- package/src/panels/storyboard/ReferencePreview.tsx +217 -0
- package/src/panels/storyboard/ShotBrief.tsx +360 -0
- package/src/panels/storyboard/ShotCard.tsx +251 -0
- package/src/panels/storyboard/StagePreview.tsx +348 -0
- package/src/panels/storyboard/StillThumbs.tsx +162 -0
- package/src/panels/storyboard/StoryboardView.tsx +79 -0
- package/src/panels/storyboard/board.css +1065 -0
- package/src/panels/storyboard/board.ts +12 -0
- package/src/panels/storyboard/boardClipboard.ts +111 -0
- package/src/panels/storyboard/boardOrder.ts +68 -0
- package/src/panels/storyboard/boardRows.tsx +425 -0
- package/src/panels/storyboard/liveCap.ts +90 -0
- package/src/panels/storyboard/plan.css +295 -0
- package/src/panels/storyboard/posterCache.ts +133 -0
- package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
- package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
- package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
- package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
- package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
- package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
- package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
- package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
- package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
- package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
- package/src/panels/storyboard/shotboard/sketch.css +1392 -0
- package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
- package/src/panels/storyboard/storyboardDrop.ts +164 -0
- package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
- package/src/panels/studio/ComponentSidePanel.tsx +355 -0
- package/src/panels/studio/ComponentStudio.tsx +243 -0
- package/src/panels/studio/ComponentsHome.tsx +318 -0
- package/src/panels/studio/ComponentsPreview.tsx +179 -0
- package/src/panels/studio/EveryUse.tsx +244 -0
- package/src/panels/studio/ThisUsePanel.tsx +679 -0
- package/src/panels/studio/componentParts.tsx +341 -0
- package/src/panels/studio/components.ts +6 -0
- package/src/panels/studio/newCodeComponent.ts +66 -0
- package/src/panels/timeline/AudioFxButton.tsx +290 -0
- package/src/panels/timeline/AudioLane.tsx +674 -85
- package/src/panels/timeline/BlockRow.tsx +154 -85
- package/src/panels/timeline/ChannelRow.tsx +378 -129
- package/src/panels/timeline/CurvePane.tsx +508 -0
- package/src/panels/timeline/Filmstrip.tsx +361 -0
- package/src/panels/timeline/GenerateDrawer.tsx +407 -0
- package/src/panels/timeline/KeyStretch.tsx +115 -0
- package/src/panels/timeline/LifespanRow.tsx +730 -47
- package/src/panels/timeline/MarkerLane.tsx +46 -8
- package/src/panels/timeline/PlayButton.tsx +91 -0
- package/src/panels/timeline/PropertyPicker.tsx +254 -0
- package/src/panels/timeline/ShotRow.tsx +400 -0
- package/src/panels/timeline/SoundMode.tsx +1406 -0
- package/src/panels/timeline/TimelineDock.tsx +2338 -272
- package/src/panels/timeline/TransportVolume.tsx +163 -0
- package/src/panels/timeline/beatRuler.ts +74 -0
- package/src/panels/timeline/curveModel.ts +500 -0
- package/src/panels/timeline/duckUnderVoice.ts +68 -0
- package/src/panels/timeline/keyframeMenu.ts +138 -0
- package/src/panels/timeline/labelWidth.ts +49 -0
- package/src/panels/timeline/marquee.ts +155 -0
- package/src/panels/timeline/model.ts +1860 -199
- package/src/panels/timeline/snap.ts +125 -43
- package/src/panels/timeline/staggerPreview.tsx +92 -0
- package/src/panels/timeline/takeMeasure.ts +72 -0
- package/src/panels/timeline/useLive.ts +48 -0
- package/src/panels/timeline/viewport.ts +234 -0
- package/src/panels/toolbar/BackChip.tsx +116 -0
- package/src/panels/toolbar/CameraViewBar.tsx +118 -49
- package/src/panels/toolbar/CanvasBar.tsx +117 -0
- package/src/panels/toolbar/InsertButton.tsx +38 -0
- package/src/panels/toolbar/IsolationTrail.tsx +156 -0
- package/src/panels/toolbar/PanelToggles.tsx +86 -0
- package/src/panels/toolbar/ShadingBar.tsx +119 -26
- package/src/panels/toolbar/ToolDockBar.tsx +329 -80
- package/src/panels/toolbar/ToolFooter.tsx +102 -0
- package/src/panels/toolbar/ViewModeBar.tsx +247 -38
- package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
- package/src/panels/toolbar/useAddRows.tsx +326 -0
- package/src/panels/toolbar/useDismiss.ts +122 -0
- package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
- package/src/panels/versions/versionModel.ts +103 -0
- package/src/panels/virtualWindow.ts +271 -0
- package/src/shotTokens.css +151 -60
- package/src/stage/FrustumGizmo.tsx +504 -84
- package/src/stage/GridPlane.tsx +225 -16
- package/src/stage/NodePreviews.tsx +628 -0
- package/src/stage/PerfHud.tsx +437 -47
- package/src/stage/SplitRenderer.tsx +816 -169
- package/src/stage/StageApp.tsx +4274 -486
- package/src/stage/StageRootBoundary.tsx +114 -0
- package/src/stage/StageTools.tsx +651 -151
- package/src/stage/TransformGizmo.tsx +696 -0
- package/src/stage/adaptivePresentation.ts +145 -0
- package/src/stage/bridgeClient.ts +2522 -41
- package/src/stage/exportWorker.ts +9 -0
- package/src/stage/feed.ts +305 -14
- package/src/stage/fontPreview.ts +176 -0
- package/src/stage/frustumGeometry.ts +319 -1
- package/src/stage/furnitureTokens.ts +33 -0
- package/src/stage/gizmoMath.ts +508 -16
- package/src/stage/gpuTimer.ts +276 -0
- package/src/stage/instanceModules.ts +194 -33
- package/src/stage/interactions/move.ts +24 -5
- package/src/stage/isolation.ts +291 -33
- package/src/stage/jsSelfProfiler.ts +325 -0
- package/src/stage/main.tsx +116 -16
- package/src/stage/modalState.ts +222 -23
- package/src/stage/moduleEpoch.ts +60 -0
- package/src/stage/motionPathStage.ts +279 -0
- package/src/stage/motionPreview.ts +74 -0
- package/src/stage/overlays/Overlays.tsx +1112 -159
- package/src/stage/overlays/Rulers.tsx +495 -0
- package/src/stage/panes.ts +159 -10
- package/src/stage/project.ts +68 -3
- package/src/stage/renderScaleController.ts +730 -0
- package/src/stage/rulers.ts +129 -0
- package/src/stage/sceneExtent.ts +75 -0
- package/src/stage/shading.ts +20 -0
- package/src/stage/splineStage.ts +632 -0
- package/src/stage/stagePerformanceTelemetry.ts +717 -0
- package/src/stage/stageTheme.ts +36 -0
- package/src/stage/stageWarmup.ts +11 -0
- package/src/stage/textAutoSize.ts +124 -0
- package/src/stage/tier1Export.ts +641 -0
- package/src/stage/tokenColor.ts +14 -9
- package/src/stage/view.ts +56 -9
- package/src/stage/workingView.ts +277 -33
- package/src/state/accountPrefs.ts +261 -0
- package/src/state/agentActions.ts +173 -15
- package/src/state/agentActivity.ts +198 -0
- package/src/state/agentQuestion.ts +239 -0
- package/src/state/bootOpen.ts +42 -0
- package/src/state/cameraVerbs.ts +139 -0
- package/src/state/clientId.ts +6 -1
- package/src/state/comments.ts +1099 -0
- package/src/state/context.tsx +19 -1
- package/src/state/editorPrefs.ts +220 -0
- package/src/state/errorReports.ts +121 -0
- package/src/state/fieldWrite.ts +161 -0
- package/src/state/fontRecents.ts +70 -0
- package/src/state/fonts.ts +656 -0
- package/src/state/graphClipboard.ts +414 -0
- package/src/state/imageThumbs.ts +216 -0
- package/src/state/insert.ts +444 -67
- package/src/state/isolationTrail.ts +92 -0
- package/src/state/keyMoves.ts +134 -0
- package/src/state/keyframeClipboard.ts +177 -0
- package/src/state/keymap.ts +1153 -117
- package/src/state/layoutPrefs.ts +272 -0
- package/src/state/layouts.ts +205 -0
- package/src/state/measureMedia.ts +167 -0
- package/src/state/menu.ts +2341 -28
- package/src/state/motionStamp.ts +7 -0
- package/src/state/panelSize.ts +375 -5
- package/src/state/pendingChanges.ts +119 -0
- package/src/state/playHold.ts +78 -0
- package/src/state/playbackPreparation.ts +562 -0
- package/src/state/posterCapture.ts +242 -0
- package/src/state/presence.ts +254 -0
- package/src/state/projectComponents.ts +160 -0
- package/src/state/renderEvents.ts +298 -18
- package/src/state/runtimeErrors.ts +46 -0
- package/src/state/selection.ts +12 -0
- package/src/state/shortcuts.ts +1606 -0
- package/src/state/sketchGuide.ts +51 -0
- package/src/state/splineRefs.ts +87 -0
- package/src/state/stageHealth.ts +259 -0
- package/src/state/stageWatchdog.ts +47 -0
- package/src/state/stillStage.ts +330 -0
- package/src/state/stillThumbs.ts +474 -0
- package/src/state/store.ts +23591 -2982
- package/src/state/storyboardModel.ts +528 -0
- package/src/state/sync.ts +1257 -0
- package/src/state/syncCache.ts +159 -0
- package/src/state/triangles.ts +13 -0
- package/src/state/types.ts +1698 -55
- package/src/state/unsavedGuard.ts +49 -0
- package/src/state/viewMode.ts +200 -0
- package/src/state/webmcp.ts +1128 -0
- package/src/state/workspace.ts +1147 -0
- package/src/styles/assets-tab.css +304 -0
- package/src/styles/canvas.css +545 -0
- package/src/styles/comments.css +1435 -0
- package/src/styles/controls.css +162 -0
- package/src/styles/dialogs.css +390 -0
- package/src/styles/insert-drawer.css +586 -0
- package/src/styles/inspector-board.css +207 -0
- package/src/styles/inspector-object.css +486 -0
- package/src/styles/inspector-shot.css +252 -0
- package/src/styles/inspector.css +360 -0
- package/src/styles/left-column.css +1628 -0
- package/src/styles/library-preview.css +575 -0
- package/src/styles/library.css +512 -0
- package/src/styles/modes/code.css +569 -0
- package/src/styles/modes/component.css +1561 -0
- package/src/styles/modes/material.css +1201 -0
- package/src/styles/modes/storyboard.css +518 -0
- package/src/styles/share-component.css +49 -0
- package/src/styles/shell.css +653 -0
- package/src/styles/shot-brief.css +360 -0
- package/src/styles/states.css +76 -0
- package/src/styles/sync.css +380 -0
- package/src/styles/timeline-graph.css +384 -0
- package/src/styles/timeline-meters.css +89 -0
- package/src/styles/timeline-rows.css +1959 -0
- package/src/styles/timeline-sound.css +1003 -0
- package/src/styles/timeline.css +754 -0
- package/src/styles/tool-footer.css +525 -0
- package/src/styles/topbar.css +886 -0
- package/src/styles/versions.css +262 -0
- package/src/styles.css +72 -2921
- package/src/tools.ts +108 -23
- package/dist/assets/index-B7notge7.css +0 -1
- package/dist/assets/index-BBE6Ta9L.js +0 -52
- package/src/panels/files/icons.tsx +0 -118
- package/src/panels/header/DocSwitcher.tsx +0 -85
- package/src/panels/header/TransportBar.tsx +0 -86
- package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
- package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
- package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
- package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
- package/src/panels/inspector/animate/useBlocks.ts +0 -43
- package/src/panels/material/MaterialPicker.tsx +0 -172
- package/src/panels/timeline/SceneStrip.tsx +0 -145
- package/src/panels/toolbar/AddMenu.tsx +0 -230
- package/src/stage/MoveGizmo.tsx +0 -411
package/src/api.ts
CHANGED
|
@@ -4,21 +4,40 @@
|
|
|
4
4
|
// back labelled and this chrome can tell its own echo from someone else's write
|
|
5
5
|
// (see state/clientId.ts). Every write also carries `baseRev`, so a lost race
|
|
6
6
|
// is a 409 the author sees rather than a silent clobber.
|
|
7
|
-
import
|
|
7
|
+
import { PROJECT_MEDIA_LIMIT_BYTES } from "@dailies-studio/shot/doc";
|
|
8
|
+
import type { DefClosure, OpenverseSource3D } from "@dailies-studio/shot/doc";
|
|
9
|
+
import type {
|
|
10
|
+
ComponentDef3D,
|
|
11
|
+
ComponentUse,
|
|
12
|
+
Doc3D,
|
|
13
|
+
ExportFormat,
|
|
14
|
+
ExportQualityId,
|
|
15
|
+
LibraryReferenceKind3D,
|
|
16
|
+
MaterialGraph3D,
|
|
17
|
+
MediaProbe,
|
|
18
|
+
RenderStats,
|
|
19
|
+
ShotAsset,
|
|
20
|
+
ShotComponentHost,
|
|
21
|
+
SyncSnapshot,
|
|
22
|
+
} from "@dailies-studio/shot/doc";
|
|
8
23
|
import type { Contract } from "@dailies-studio/contract";
|
|
24
|
+
import type { ComponentSource } from "@dailies-studio/ui";
|
|
9
25
|
import { createAgentClient } from "@dailies-studio/agent-client";
|
|
10
26
|
import type { AgentClient, AgentTransport } from "@dailies-studio/agent-client";
|
|
11
27
|
import { CLIENT_ID, CLIENT_ID_HEADER } from "./state/clientId.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
name: string;
|
|
15
|
-
mtimeMs: number;
|
|
16
|
-
}
|
|
28
|
+
import { dailiesHost } from "./hostApi.js";
|
|
29
|
+
import type { DailiesHost, HostRefusalError } from "./hostApi.js";
|
|
17
30
|
|
|
18
31
|
export interface LoadedShot {
|
|
19
32
|
name: string;
|
|
20
33
|
doc: Doc3D;
|
|
21
34
|
rev: string;
|
|
35
|
+
/** What the caller may do with this Project. Absent (an older server)
|
|
36
|
+
* means everything; hosted, a Viewer reads `edit: false`. */
|
|
37
|
+
can?: { edit: boolean; comment: boolean };
|
|
38
|
+
/** Live collaboration is on for this Project: edits go over the document's
|
|
39
|
+
* live session (state/sync.ts), not `saveShot`. */
|
|
40
|
+
sync?: true;
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
export interface SaveResult {
|
|
@@ -36,6 +55,9 @@ export class ApiError extends Error {
|
|
|
36
55
|
readonly status: number,
|
|
37
56
|
message: string,
|
|
38
57
|
readonly reason?: string,
|
|
58
|
+
readonly details?: Readonly<Record<string, unknown>>,
|
|
59
|
+
/** The body's machine `code`, e.g. "forbidden" for a role refusal. */
|
|
60
|
+
readonly code?: string,
|
|
39
61
|
) {
|
|
40
62
|
super(message);
|
|
41
63
|
this.name = "ApiError";
|
|
@@ -45,72 +67,268 @@ export class ApiError extends Error {
|
|
|
45
67
|
}
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
/**
|
|
71
|
+
* How long a request waits out a workspace that is starting, and how often it
|
|
72
|
+
* asks again: the dashboard's budget (packages/dashboard/src/api.ts). The
|
|
73
|
+
* hosted Worker answers JSON 503 `{starting: true}` while the workspace's
|
|
74
|
+
* store resets after a deploy; any other 503 is a real failure.
|
|
75
|
+
*/
|
|
76
|
+
export const STARTING_BUDGET_MS = 20_000;
|
|
77
|
+
export const STARTING_RETRY_MS = 1_500;
|
|
78
|
+
|
|
79
|
+
/** `baseRevGuarded`: a write the store refuses as stale (409) once a first
|
|
80
|
+
* attempt landed, so sending it again after a 503 `{starting: true}` cannot
|
|
81
|
+
* do twice what the first did. A read is retried too; nothing else is.
|
|
82
|
+
* `onStatus` hears the final answer's status, for a route whose 200 and 201
|
|
83
|
+
* mean different things. */
|
|
84
|
+
async function request<T>(
|
|
85
|
+
path: string,
|
|
86
|
+
init?: RequestInit,
|
|
87
|
+
baseRevGuarded = false,
|
|
88
|
+
onStatus?: (status: number) => void,
|
|
89
|
+
): Promise<T> {
|
|
90
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
91
|
+
const retries = baseRevGuarded || method === "GET" || method === "HEAD";
|
|
92
|
+
const deadline = Date.now() + STARTING_BUDGET_MS;
|
|
93
|
+
let res: Response;
|
|
94
|
+
let body: unknown;
|
|
95
|
+
for (;;) {
|
|
96
|
+
res = await fetch(path, {
|
|
97
|
+
...init,
|
|
98
|
+
headers: {
|
|
99
|
+
...(init?.body !== undefined ? { "content-type": "application/json" } : {}),
|
|
100
|
+
[CLIENT_ID_HEADER]: CLIENT_ID,
|
|
101
|
+
...init?.headers,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
body = null;
|
|
106
|
+
if (text.length > 0) {
|
|
107
|
+
try {
|
|
108
|
+
body = JSON.parse(text);
|
|
109
|
+
} catch {
|
|
110
|
+
// A non-JSON body from a route that owes JSON is itself the error.
|
|
111
|
+
throw new ApiError(res.status, text.slice(0, 400));
|
|
112
|
+
}
|
|
65
113
|
}
|
|
114
|
+
const starting = res.status === 503 && (body as { starting?: unknown } | null)?.starting === true;
|
|
115
|
+
if (!starting || !retries || Date.now() + STARTING_RETRY_MS > deadline) break;
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, STARTING_RETRY_MS));
|
|
66
117
|
}
|
|
67
118
|
if (!res.ok) {
|
|
68
|
-
const b = body as { error?: unknown; reason?: unknown } | null;
|
|
119
|
+
const b = body as { error?: unknown; reason?: unknown; code?: unknown } | null;
|
|
120
|
+
// The whole body rides along, so a caller can read a refusal's extras
|
|
121
|
+
// (`userIds` on a 422 mention_no_access, `retry` on a 429).
|
|
122
|
+
const details = typeof body === "object" && body !== null ? (body as Readonly<Record<string, unknown>>) : undefined;
|
|
69
123
|
throw new ApiError(
|
|
70
124
|
res.status,
|
|
71
|
-
|
|
125
|
+
details !== undefined && b?.code === "project_quota_exceeded"
|
|
126
|
+
? quotaMessage(details)
|
|
127
|
+
: typeof b?.error === "string"
|
|
128
|
+
? b.error
|
|
129
|
+
: `${init?.method ?? "GET"} ${path} failed (${res.status})`,
|
|
72
130
|
typeof b?.reason === "string" ? b.reason : undefined,
|
|
131
|
+
details,
|
|
132
|
+
typeof b?.code === "string" ? b.code : undefined,
|
|
73
133
|
);
|
|
74
134
|
}
|
|
135
|
+
onStatus?.(res.status);
|
|
75
136
|
return body as T;
|
|
76
137
|
}
|
|
77
138
|
|
|
78
|
-
export function listShots(): Promise<{ shots: ShotListing[] }> {
|
|
79
|
-
return request("/api/shots");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
139
|
export function loadShot(name: string): Promise<LoadedShot> {
|
|
83
140
|
return request(`/api/shots/${encodeURIComponent(name)}`);
|
|
84
141
|
}
|
|
85
142
|
|
|
143
|
+
/** The live document with its place in the session's log: what a snapshot
|
|
144
|
+
* too large for the socket points at. */
|
|
145
|
+
export function loadShotSnapshot(name: string): Promise<SyncSnapshot> {
|
|
146
|
+
return request(`/api/shots/${encodeURIComponent(name)}?sync=1`);
|
|
147
|
+
}
|
|
148
|
+
|
|
86
149
|
/** `baseRev` is REQUIRED at this seam even though the route allows it to be
|
|
87
150
|
* absent: an editor that omits it is an editor that can silently overwrite
|
|
88
151
|
* another surface's work, and `null` already means "I am claiming a create". */
|
|
89
152
|
export function saveShot(name: string, doc: Doc3D, baseRev: string | null): Promise<SaveResult> {
|
|
90
|
-
return request(
|
|
91
|
-
|
|
92
|
-
body: JSON.stringify({ doc, baseRev }),
|
|
153
|
+
return request(
|
|
154
|
+
`/api/shots/${encodeURIComponent(name)}`,
|
|
155
|
+
{ method: "PUT", body: JSON.stringify({ doc, baseRev }) },
|
|
156
|
+
true,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Re-emit `src/shots/<Name>.tsx` from the document, discarding hand edits.
|
|
162
|
+
*
|
|
163
|
+
* ALWAYS DESTRUCTIVE at this seam, and the caller must confirm before reaching
|
|
164
|
+
* it. The route's GET is the dry run and its POST without the flag is a
|
|
165
|
+
* re-emit that still refuses a conflict — but a menu row named "Regenerate" that
|
|
166
|
+
* reported "still blocked" and did nothing would leave the author exactly where
|
|
167
|
+
* the emit warning already put them, which is the state this row exists to
|
|
168
|
+
* leave. `ok: false` with `blocked` is still a 200: the answer arrived.
|
|
169
|
+
*/
|
|
170
|
+
export function regenerateShot(name: string): Promise<{
|
|
171
|
+
ok: boolean;
|
|
172
|
+
generated: string | null;
|
|
173
|
+
blocked?: string;
|
|
174
|
+
warning?: string;
|
|
175
|
+
}> {
|
|
176
|
+
return request(`/api/shots/${encodeURIComponent(name)}/regenerate`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify({ discardHandEdits: true }),
|
|
93
179
|
});
|
|
94
180
|
}
|
|
95
181
|
|
|
96
|
-
|
|
97
|
-
|
|
182
|
+
// ---- version history (packages/store/src/shotVersions.ts) --------------------
|
|
183
|
+
|
|
184
|
+
/** The open autosave group: the edits that led to the stored document. */
|
|
185
|
+
export interface ShotVersionHead {
|
|
186
|
+
/** User ids, first writer first. "local" is the CLI's one user. */
|
|
187
|
+
authors: string[];
|
|
188
|
+
/** Epoch ms of the group's first and last save. */
|
|
189
|
+
from: number;
|
|
190
|
+
at: number;
|
|
98
191
|
}
|
|
99
192
|
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
193
|
+
export interface ShotVersion extends ShotVersionHead {
|
|
194
|
+
id: string;
|
|
195
|
+
rev: string;
|
|
196
|
+
/** Present on a named version. */
|
|
197
|
+
name?: string;
|
|
198
|
+
/** The row a restore brought back. */
|
|
199
|
+
restoredFrom?: string;
|
|
105
200
|
}
|
|
106
201
|
|
|
107
|
-
export
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
202
|
+
export interface ShotVersionList {
|
|
203
|
+
head: ShotVersionHead | null;
|
|
204
|
+
/** Newest first. */
|
|
205
|
+
versions: ShotVersion[];
|
|
206
|
+
/** User id to display name (hosted). Empty from the CLI. */
|
|
207
|
+
people: Record<string, string>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const versionsPath = (name: string, rest = ""): string => `/api/shots/${encodeURIComponent(name)}/versions${rest}`;
|
|
211
|
+
|
|
212
|
+
// ---- an agent's question to the person (store agentQuestions.ts) ----------
|
|
213
|
+
|
|
214
|
+
/** What an export question opens the Export dialog with (store
|
|
215
|
+
* `AgentQuestionExport`); each absent field is the dialog's own default. */
|
|
216
|
+
export interface AgentQuestionExport {
|
|
217
|
+
format?: ExportFormat;
|
|
218
|
+
quality?: ExportQualityId;
|
|
219
|
+
scene?: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The saved video an export question's yes made (store
|
|
223
|
+
* `AgentQuestionVideo`): in Videos, written by the studio server's render, or
|
|
224
|
+
* only downloaded. */
|
|
225
|
+
export type AgentQuestionVideo =
|
|
226
|
+
| { where: "videos"; id: string; url: string }
|
|
227
|
+
| { where: "project"; path: string }
|
|
228
|
+
| { where: "downloads"; filename: string };
|
|
229
|
+
|
|
230
|
+
/** The question as the route answers it (store `AgentQuestion`, restated:
|
|
231
|
+
* the chrome does not depend on the store package). */
|
|
232
|
+
export interface AgentQuestion {
|
|
233
|
+
id: string;
|
|
234
|
+
projectId: string;
|
|
235
|
+
question: string;
|
|
236
|
+
summary: string;
|
|
237
|
+
boardRev: string;
|
|
238
|
+
askedBy: { client: string; member: string | null; sessionId: string };
|
|
239
|
+
askedAt: number;
|
|
240
|
+
status: "open" | "answered" | "withdrawn";
|
|
241
|
+
/** A later question's export: a yes opens the Export dialog with it. */
|
|
242
|
+
export?: AgentQuestionExport;
|
|
243
|
+
answer?: {
|
|
244
|
+
choice: "yes" | "changes";
|
|
245
|
+
note?: string;
|
|
246
|
+
by: { id: string; name: string };
|
|
247
|
+
at: number;
|
|
248
|
+
boardRev: string | null;
|
|
249
|
+
video?: AgentQuestionVideo;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const questionPath = (name: string): string => `/api/shots/${encodeURIComponent(name)}/question`;
|
|
254
|
+
|
|
255
|
+
/** The Project's open question, else its latest answered one for a day. */
|
|
256
|
+
export async function getAgentQuestion(name: string): Promise<AgentQuestion | null> {
|
|
257
|
+
return (await request<{ question: AgentQuestion | null }>(questionPath(name))).question;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Answer it: "yes" makes the video, "changes" sends the note. */
|
|
261
|
+
export async function answerAgentQuestion(
|
|
262
|
+
name: string,
|
|
263
|
+
id: string,
|
|
264
|
+
body: { choice: "yes" | "changes"; note?: string },
|
|
265
|
+
): Promise<AgentQuestion> {
|
|
266
|
+
return (
|
|
267
|
+
await request<{ question: AgentQuestion }>(`${questionPath(name)}/${encodeURIComponent(id)}/answer`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
body: JSON.stringify(body),
|
|
270
|
+
})
|
|
271
|
+
).question;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Tell an export question where its saved video is, which wakes the
|
|
275
|
+
* agent's waiting get_answer. */
|
|
276
|
+
export async function reportAgentQuestionVideo(name: string, id: string, video: AgentQuestionVideo): Promise<AgentQuestion> {
|
|
277
|
+
return (
|
|
278
|
+
await request<{ question: AgentQuestion }>(`${questionPath(name)}/${encodeURIComponent(id)}/video`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: JSON.stringify({ video }),
|
|
281
|
+
})
|
|
282
|
+
).question;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function listVersions(name: string): Promise<ShotVersionList> {
|
|
286
|
+
return request(versionsPath(name));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Close the open group as a version. `rev` refuses (409) when the stored
|
|
290
|
+
* document is no longer the one the author saw. */
|
|
291
|
+
export function saveVersion(name: string, body: { name?: string; rev?: string }): Promise<{ version: ShotVersion }> {
|
|
292
|
+
return request(versionsPath(name), { method: "POST", body: JSON.stringify(body) });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function readVersion(name: string, id: string): Promise<{ version: ShotVersion; doc: Doc3D }> {
|
|
296
|
+
return request(versionsPath(name, `/${encodeURIComponent(id)}`));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function renameVersion(name: string, id: string, label: string): Promise<{ version: ShotVersion }> {
|
|
300
|
+
return request(versionsPath(name, `/${encodeURIComponent(id)}`), {
|
|
301
|
+
method: "PATCH",
|
|
302
|
+
body: JSON.stringify({ name: label }),
|
|
111
303
|
});
|
|
112
304
|
}
|
|
113
305
|
|
|
306
|
+
/** Write the version's document as a new save. The live session adopts it
|
|
307
|
+
* like any other write, so the editor sees a change event. */
|
|
308
|
+
export function restoreVersion(name: string, id: string): Promise<{ version: ShotVersion; rev: string }> {
|
|
309
|
+
return request(versionsPath(name, `/${encodeURIComponent(id)}/restore`), { method: "POST" });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The Trash entries of the document `docId`. A hosted editor may list no
|
|
313
|
+
* more than that; the local server lists the whole Trash. `id` is the
|
|
314
|
+
* Project's id (hosted, where it is the document's id too); `name` the name
|
|
315
|
+
* it had (local). */
|
|
316
|
+
export function listTrash(docId: string): Promise<{ items: { trashId: string; id?: string; name?: string; deletedAt: number }[] }> {
|
|
317
|
+
return request(`/api/trash?doc=${encodeURIComponent(docId)}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Bring a Project back from the Trash (R10's "Restore"). `name` is where it
|
|
321
|
+
* landed: its old name, or a free one if that was taken meanwhile. */
|
|
322
|
+
export function restoreFromTrash(trashId: string): Promise<{ name: string }> {
|
|
323
|
+
return request(`/api/trash/${encodeURIComponent(trashId)}/restore`, { method: "POST" });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** `doc` as a new Project, beside a trashed one whose media it may use (R10's
|
|
327
|
+
* "Save as copy"). */
|
|
328
|
+
export function copyFromTrash(trashId: string, doc: Doc3D): Promise<{ name: string; id: string; title: string }> {
|
|
329
|
+
return request(`/api/trash/${encodeURIComponent(trashId)}/copy`, { method: "POST", body: JSON.stringify({ doc }) });
|
|
330
|
+
}
|
|
331
|
+
|
|
114
332
|
// ---- the component registry (instance layers) --------------------------------
|
|
115
333
|
|
|
116
334
|
/**
|
|
@@ -122,7 +340,10 @@ export function duplicateShot(name: string, to: string): Promise<{ duplicated: t
|
|
|
122
340
|
*
|
|
123
341
|
* `host` is the instance gate (only "three" components can be instanced — a
|
|
124
342
|
* "dom" one under the three reconciler is a hard crash, and the SERVER's
|
|
125
|
-
* `classifyHost` is what decided the value).
|
|
343
|
+
* `classifyHost` is what decided the value). Its union is `ShotComponentHost`,
|
|
344
|
+
* imported rather than restated: the row is the wire shape of a value the
|
|
345
|
+
* schema already owns, and a chrome-local copy would keep typechecking after a
|
|
346
|
+
* host classification was added. `contractHash` is the drift
|
|
126
347
|
* baseline recorded on the layer. `contractAvailability` is the server's own
|
|
127
348
|
* verdict on whether generated code can STATICALLY IMPORT the contract or
|
|
128
349
|
* whether the document must carry a snapshot — consumed verbatim, never
|
|
@@ -137,7 +358,14 @@ export interface ComponentRow {
|
|
|
137
358
|
contractHash: string;
|
|
138
359
|
contractSource: "declared" | "inferred";
|
|
139
360
|
contractAvailability?: "sibling" | "colocated" | "inferred";
|
|
140
|
-
host:
|
|
361
|
+
host: ShotComponentHost;
|
|
362
|
+
/**
|
|
363
|
+
* The SERVER's verdict on where the component came from: the user's own code
|
|
364
|
+
* or something the studio shipped. `builtin` is the older flag and both are
|
|
365
|
+
* on the wire, so read them through `componentSourceOf` from
|
|
366
|
+
* `@dailies-studio/ui` rather than testing either one directly.
|
|
367
|
+
*/
|
|
368
|
+
source?: ComponentSource;
|
|
141
369
|
builtin?: boolean;
|
|
142
370
|
/**
|
|
143
371
|
* Present only on catalog EFFECTS, which is what makes it the filter for the
|
|
@@ -155,10 +383,241 @@ export interface ComponentRow {
|
|
|
155
383
|
thumb?: string;
|
|
156
384
|
softwareRaster?: "unfinished";
|
|
157
385
|
};
|
|
386
|
+
/**
|
|
387
|
+
* Where the SERVER says this component came from: the user's own code, a
|
|
388
|
+
* shipped built-in, or a library item this Project vendored under
|
|
389
|
+
* `src/shared/`. Derived from the install lockfile at read time, which is
|
|
390
|
+
* why the document never stores it: a field would go stale on uninstall.
|
|
391
|
+
*/
|
|
392
|
+
library?: LibraryDescriptor;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---- the shipped library -----------------------------------------------------
|
|
396
|
+
//
|
|
397
|
+
// Chrome-local copies of the CLI's `libraryCatalog.ts` shapes, for the same
|
|
398
|
+
// reason `ComponentRow` is declared above rather than imported: only these
|
|
399
|
+
// fields are load-bearing here, and the shot chrome depends on no other package
|
|
400
|
+
// for a wire shape. Every field the server ADDS later keeps this typechecking.
|
|
401
|
+
|
|
402
|
+
export type LibraryScope = "builtin" | "shared" | "project";
|
|
403
|
+
|
|
404
|
+
export interface LibraryDescriptor {
|
|
405
|
+
scope: LibraryScope;
|
|
406
|
+
removable: boolean;
|
|
407
|
+
publisher?: string;
|
|
408
|
+
libraryId?: string;
|
|
409
|
+
libraryVersion?: number;
|
|
410
|
+
/** The installed files no longer match what was installed. */
|
|
411
|
+
modified?: boolean;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** `component`, the four kinds a `library` reference may name, and `sounds`,
|
|
415
|
+
* which is none of them: a pack is imported and its files become sound
|
|
416
|
+
* cards, so a `library` card of kind "sounds" is a type error here. */
|
|
417
|
+
export type LibraryItemKind = "component" | LibraryReferenceKind3D | "sounds";
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* One row of `GET /api/library/catalog`: a built-in, an installed shared
|
|
421
|
+
* component, or a SHIPPED item. `action` is the server's verdict on what a
|
|
422
|
+
* drop does: `install` vendors a component first, `add` places one already in
|
|
423
|
+
* the registry (`installedAs` names it), `claim` copies a shot into a new
|
|
424
|
+
* Project, `import` lands a material, an image pack or a sound pack in this
|
|
425
|
+
* one, and `read` is a blueprint, which is copied by hand through its ops.
|
|
426
|
+
*/
|
|
427
|
+
export interface LibraryItem {
|
|
428
|
+
id: string;
|
|
429
|
+
name: string;
|
|
430
|
+
/** The manifest's human name; print it through `libraryDisplayName`. */
|
|
431
|
+
displayName?: string;
|
|
432
|
+
source: "builtin" | "shared" | "shipped";
|
|
433
|
+
group: string;
|
|
434
|
+
kind?: LibraryItemKind;
|
|
435
|
+
category?: string;
|
|
436
|
+
tags: string[];
|
|
437
|
+
vibe?: string[];
|
|
438
|
+
useFor?: string;
|
|
439
|
+
avoidFor?: string;
|
|
440
|
+
lengthFrames?: number;
|
|
441
|
+
deps?: string[];
|
|
442
|
+
/** The `/api/library/items/<id>` URL. */
|
|
443
|
+
docs?: string;
|
|
444
|
+
/** The `/api/library/items/<id>/thumb` URL, only when the item has one. */
|
|
445
|
+
thumb?: string;
|
|
446
|
+
host: ShotComponentHost;
|
|
447
|
+
action: "add" | "install" | "claim" | "import" | "read";
|
|
448
|
+
installedAs?: string;
|
|
449
|
+
description?: string;
|
|
450
|
+
publisher?: string;
|
|
451
|
+
version?: number;
|
|
452
|
+
updateAvailable?: boolean;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export interface LibraryCatalog {
|
|
456
|
+
items: LibraryItem[];
|
|
457
|
+
/** Items the registry refused to read. Named so the rail can say WHICH. */
|
|
458
|
+
problems: Array<{ id: string; error: string }>;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** `GET /api/library/items/<id>`: the row, its `library.json`, its README and
|
|
462
|
+
* the kind-specific extra. */
|
|
463
|
+
export interface LibraryItemDetail {
|
|
464
|
+
item: LibraryItem;
|
|
465
|
+
manifest: Readonly<Record<string, unknown>>;
|
|
466
|
+
docs: string;
|
|
467
|
+
component?: {
|
|
468
|
+
files: Array<{ path: string; bytes: number; hash: string }>;
|
|
469
|
+
peerDeps: string[];
|
|
470
|
+
entry: string;
|
|
471
|
+
exportName: string;
|
|
472
|
+
host: ShotComponentHost;
|
|
473
|
+
contractHash: string;
|
|
474
|
+
installedAs?: string;
|
|
475
|
+
contract?: Contract;
|
|
476
|
+
};
|
|
477
|
+
shot?: { summary: { durationFrames: number; sceneCount: number; layerCount: number; outline: string[] } };
|
|
478
|
+
material?: { material: MaterialGraph3D };
|
|
479
|
+
blueprint?: { ops: unknown; expects: Array<{ layer: string; kind: string }> };
|
|
480
|
+
images?: { files: Array<{ path: string; kind: string; bytes: number }> };
|
|
481
|
+
/** A sound pack's files, each measured off its WAV: its length and where
|
|
482
|
+
* it lands (`hitMs`, from its `library.json`). */
|
|
483
|
+
sounds?: {
|
|
484
|
+
files: Array<{
|
|
485
|
+
path: string;
|
|
486
|
+
kind: "audio";
|
|
487
|
+
bytes: number;
|
|
488
|
+
durationMs: number;
|
|
489
|
+
hitMs: number;
|
|
490
|
+
/** The author's name for the file (`library.json`); print it through
|
|
491
|
+
* `libraryFileLabel`, which falls back to the stem. */
|
|
492
|
+
label?: string;
|
|
493
|
+
}>;
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** One sound a pack import registered: the Project asset id `add_audio` and a
|
|
498
|
+
* sound card name, the pack's file name, and the two numbers it is placed by. */
|
|
499
|
+
export interface LibraryAddedSound {
|
|
500
|
+
id: string;
|
|
501
|
+
file: string;
|
|
502
|
+
hitMs: number;
|
|
503
|
+
durationMs: number;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** `POST /api/library/add-sounds`: the whole pack, content-addressed, so a
|
|
507
|
+
* second import of the same pack uploads nothing and answers the same ids. */
|
|
508
|
+
export interface LibraryAddSoundsResult {
|
|
509
|
+
assets: LibraryAddedSound[];
|
|
510
|
+
rev: string;
|
|
511
|
+
usedBytes: number;
|
|
512
|
+
remainingBytes: number;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** `POST /api/library/install`: the CLI's `InstallResult`, plus the component
|
|
516
|
+
* detail the route fetched AFTER awaiting its own registry rescan, so the
|
|
517
|
+
* caller can place the component without a second round trip. */
|
|
518
|
+
export interface LibraryInstallResult {
|
|
519
|
+
componentId: string;
|
|
520
|
+
dir: string;
|
|
521
|
+
written: string[];
|
|
522
|
+
removed: string[];
|
|
523
|
+
alreadyInstalled: boolean;
|
|
524
|
+
replaces: { version: number; contentHash: string } | null;
|
|
525
|
+
files: Array<{ path: string }>;
|
|
526
|
+
conflicts: string[];
|
|
527
|
+
componentDetail: ComponentDetail | null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function libraryCatalog(): Promise<LibraryCatalog> {
|
|
531
|
+
return request("/api/library/catalog");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function libraryItem(id: string): Promise<LibraryItemDetail> {
|
|
535
|
+
return request(`/api/library/items/${encodeURIComponent(id)}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Vendor a shipped component into this Project. A 409 is a user file in the
|
|
539
|
+
* way, refused before any byte lands. */
|
|
540
|
+
export function libraryInstall(libraryId: string): Promise<LibraryInstallResult> {
|
|
541
|
+
return request("/api/library/install", {
|
|
542
|
+
method: "POST",
|
|
543
|
+
body: JSON.stringify({ libraryId }),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Copy a shipped shot into a NEW Project. 409 when `name` is taken. */
|
|
548
|
+
export function libraryClaim(
|
|
549
|
+
libraryId: string,
|
|
550
|
+
name?: string,
|
|
551
|
+
): Promise<{ name: string; title: string; projectId: string; href: string }> {
|
|
552
|
+
return request("/api/library/claim", {
|
|
553
|
+
method: "POST",
|
|
554
|
+
body: JSON.stringify(name === undefined ? { libraryId } : { libraryId, name }),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** `baseRev` is the rev from the last read, so a stale write is a 409 the
|
|
559
|
+
* author sees rather than a silent clobber: the same discipline as `saveShot`. */
|
|
560
|
+
export function libraryImportMaterial(input: {
|
|
561
|
+
libraryId: string;
|
|
562
|
+
name: string;
|
|
563
|
+
baseRev: string;
|
|
564
|
+
}): Promise<{ materialId: string; rev: string }> {
|
|
565
|
+
return request("/api/library/import-material", {
|
|
566
|
+
method: "POST",
|
|
567
|
+
body: JSON.stringify(input),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function libraryAddImages(input: {
|
|
572
|
+
libraryId: string;
|
|
573
|
+
name: string;
|
|
574
|
+
baseRev: string;
|
|
575
|
+
}): Promise<{
|
|
576
|
+
assets: Array<{ id: string; path: string; hash: string; kind: string; size: number }>;
|
|
577
|
+
rev: string;
|
|
578
|
+
usedBytes: number;
|
|
579
|
+
remainingBytes: number;
|
|
580
|
+
}> {
|
|
581
|
+
return request("/api/library/add-images", {
|
|
582
|
+
method: "POST",
|
|
583
|
+
body: JSON.stringify(input),
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Import a sound pack into a Project against a saved rev: the one sound
|
|
588
|
+
* `file` names (a bare file name), or every file of it when it names none. */
|
|
589
|
+
export function libraryAddSounds(input: {
|
|
590
|
+
libraryId: string;
|
|
591
|
+
name: string;
|
|
592
|
+
baseRev: string;
|
|
593
|
+
file?: string;
|
|
594
|
+
}): Promise<LibraryAddSoundsResult> {
|
|
595
|
+
return request("/api/library/add-sounds", {
|
|
596
|
+
method: "POST",
|
|
597
|
+
body: JSON.stringify(input),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Where a default edit would LAND and which keys can take one — the defaults
|
|
603
|
+
* rail renders read-only rows from this instead of learning editability from a
|
|
604
|
+
* refused write. Computed by the server against the current source bytes on
|
|
605
|
+
* every detail GET, never cached beside the contract (the answer can change
|
|
606
|
+
* without the contractHash moving). Absent on builtins.
|
|
607
|
+
*/
|
|
608
|
+
export interface ComponentDefaultsMeta {
|
|
609
|
+
/** Project-relative posix path the persist write targets: the sibling
|
|
610
|
+
* `.controls.ts`, the component file (colocated / inferred). */
|
|
611
|
+
file: string;
|
|
612
|
+
/** False when the target sits in a generated lane — the whole rail is
|
|
613
|
+
* read-only then, with the shot named as the owner. */
|
|
614
|
+
writable: boolean;
|
|
615
|
+
keys: Readonly<Record<string, { editable: true } | { editable: false; reason: string }>>;
|
|
158
616
|
}
|
|
159
617
|
|
|
160
618
|
export interface ComponentDetail extends ComponentRow {
|
|
161
619
|
contract: Contract;
|
|
620
|
+
defaults?: ComponentDefaultsMeta;
|
|
162
621
|
}
|
|
163
622
|
|
|
164
623
|
/** A bare ARRAY, not an envelope — the shape that route has always answered. */
|
|
@@ -170,37 +629,382 @@ export function componentDetail(id: string): Promise<ComponentDetail> {
|
|
|
170
629
|
return request(`/api/components/${encodeURIComponent(id)}`);
|
|
171
630
|
}
|
|
172
631
|
|
|
632
|
+
/** What persisting a default answers. */
|
|
633
|
+
export interface ComponentDefaultResult {
|
|
634
|
+
file: string;
|
|
635
|
+
wrote: boolean;
|
|
636
|
+
hash?: string;
|
|
637
|
+
results: Readonly<Record<string, { status: "written" } | { status: "refused"; reason: string }>>;
|
|
638
|
+
contractHash: string;
|
|
639
|
+
editableKeys: ComponentDefaultsMeta["keys"];
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Persist ONE contract default into the component's source — the defaults
|
|
644
|
+
* rail's commit. The server locates the exact span (the `default:` inside
|
|
645
|
+
* `defineControls`, or the destructured render-fn default) and splices the
|
|
646
|
+
* printed literal; everything else in the file survives byte-for-byte.
|
|
647
|
+
*
|
|
648
|
+
* `contractHash` is the drift baseline: a 409 means the contract moved since
|
|
649
|
+
* the panel loaded, and the caller refetches the detail rather than writing
|
|
650
|
+
* blind. The per-key verdict comes back in `results` on a 200 — a refused key
|
|
651
|
+
* is an answer, not an error.
|
|
652
|
+
*
|
|
653
|
+
* Hosted, the code host computes the edit (`__dailiesHost.applyDefaults`,
|
|
654
|
+
* the same decisions over the files it compiled), this module writes it
|
|
655
|
+
* through the file API with its baseHash, and the host re-extracts to answer
|
|
656
|
+
* the contract after it: the dev server's route, split at the write.
|
|
657
|
+
*/
|
|
658
|
+
export function setComponentDefault(
|
|
659
|
+
id: string,
|
|
660
|
+
contractHash: string,
|
|
661
|
+
key: string,
|
|
662
|
+
value: unknown,
|
|
663
|
+
): Promise<ComponentDefaultResult> {
|
|
664
|
+
const host = dailiesHost();
|
|
665
|
+
if (host !== undefined) return hostedDefault(host, id, { contractHash, values: { [key]: value } });
|
|
666
|
+
return request(`/api/components/${encodeURIComponent(id)}/defaults`, {
|
|
667
|
+
method: "POST",
|
|
668
|
+
body: JSON.stringify({ contractHash, values: { [key]: value } }),
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** A host refusal as the error the dev server's route gives this module:
|
|
673
|
+
* its status and message, and its `reason` when it carries one. Any other
|
|
674
|
+
* failure passes through. */
|
|
675
|
+
function hostError(error: unknown): unknown {
|
|
676
|
+
const status = (error as HostRefusalError | null)?.status;
|
|
677
|
+
if (!(error instanceof Error) || typeof status !== "number") return error;
|
|
678
|
+
const reason = (error as HostRefusalError).body?.["reason"];
|
|
679
|
+
return new ApiError(status, error.message, typeof reason === "string" ? reason : undefined);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function viaHost<T>(call: () => Promise<T>): Promise<T> {
|
|
683
|
+
try {
|
|
684
|
+
return await call();
|
|
685
|
+
} catch (e) {
|
|
686
|
+
throw hostError(e);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** The dev server's defaults route, hosted: edit, write with the edit's
|
|
691
|
+
* baseHash, and on a 409 (the file moved under the edit) edit again and
|
|
692
|
+
* write ONCE more, so a second collision is the author's to see, as there. */
|
|
693
|
+
async function hostedDefault(
|
|
694
|
+
host: DailiesHost,
|
|
695
|
+
id: string,
|
|
696
|
+
body: { contractHash: string; values: Record<string, unknown> },
|
|
697
|
+
): Promise<ComponentDefaultResult> {
|
|
698
|
+
let edit = await viaHost(() => host.applyDefaults(id, body));
|
|
699
|
+
let written: SourceFileSaved | null = null;
|
|
700
|
+
if (edit.edits > 0) {
|
|
701
|
+
try {
|
|
702
|
+
written = await writeFile(edit.file, edit.content, edit.baseHash);
|
|
703
|
+
} catch (e) {
|
|
704
|
+
if (!(e instanceof ApiError) || e.status !== 409) throw e;
|
|
705
|
+
edit = await viaHost(() => host.applyDefaults(id, body));
|
|
706
|
+
if (edit.edits > 0) written = await writeFile(edit.file, edit.content, edit.baseHash);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// The contract the host serves now, the write extracted.
|
|
710
|
+
const after = await viaHost(() => host.componentDefaults(id));
|
|
711
|
+
return {
|
|
712
|
+
file: edit.file,
|
|
713
|
+
wrote: written !== null,
|
|
714
|
+
...(written !== null ? { hash: written.hash } : {}),
|
|
715
|
+
results: edit.results,
|
|
716
|
+
contractHash: after?.contractHash ?? edit.contractHash,
|
|
717
|
+
editableKeys: after?.editableKeys ?? {},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ---- code component source ---------------------------------------------------
|
|
722
|
+
//
|
|
723
|
+
// `/api/files` is the studio's ONE write path into user code, and it is the
|
|
724
|
+
// same route the motion chrome's code drawer has always used. Nothing new is
|
|
725
|
+
// added server-side for the code studio: the containment guard, the generated
|
|
726
|
+
// guard, the sha256 conflict check and the atomic write are all already there,
|
|
727
|
+
// and reaching them through anything but this route would be a second set of
|
|
728
|
+
// rules over the same files.
|
|
729
|
+
|
|
730
|
+
/** Mirror of the server's FileDiagnostic. Positions are 0-based offsets into
|
|
731
|
+
* the text that was checked, which is the buffer when one was sent. */
|
|
732
|
+
export interface CodeDiagnostic {
|
|
733
|
+
category: "error" | "warning" | "suggestion" | "message";
|
|
734
|
+
code: number;
|
|
735
|
+
message: string;
|
|
736
|
+
start: number;
|
|
737
|
+
length: number;
|
|
738
|
+
line: number;
|
|
739
|
+
character: number;
|
|
740
|
+
endLine: number;
|
|
741
|
+
endCharacter: number;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export interface SourceFile {
|
|
745
|
+
path: string;
|
|
746
|
+
content: string;
|
|
747
|
+
/** sha256 of the bytes on disk. Sent back as `baseHash` to make a write
|
|
748
|
+
* conflict-checked, which is how a save can refuse rather than clobber. */
|
|
749
|
+
hash: string;
|
|
750
|
+
mtimeMs: number;
|
|
751
|
+
size: number;
|
|
752
|
+
/** What the file uses on disk, so a save can put it back. */
|
|
753
|
+
lineEnding: "lf" | "crlf";
|
|
754
|
+
/** Set when the file is a build product the studio itself emits. */
|
|
755
|
+
generated?: { lane: string; by: string; id: string; docName?: string };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export interface SourceFileSaved {
|
|
759
|
+
saved: true;
|
|
760
|
+
hash: string;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** One editable file in the project, as the listing reports it. FLAT — the
|
|
764
|
+
* server walks the tree and answers posix paths, and the shape of the tree is
|
|
765
|
+
* the chrome's problem, which is right: nesting is a way of DRAWING this list,
|
|
766
|
+
* not a fact about the project. */
|
|
767
|
+
export interface ProjectFile {
|
|
768
|
+
path: string;
|
|
769
|
+
size: number;
|
|
770
|
+
/** Set when the file is a build product the studio itself emits. The listing
|
|
771
|
+
* decides this by directory alone, so a marker-only generated file reads as
|
|
772
|
+
* ordinary here and refuses on the way in — which is why the buffer trusts
|
|
773
|
+
* `SourceFile.generated` and not this. */
|
|
774
|
+
generated?: { lane: string; by: string; id: string; docName?: string };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** `truncated` means the walk hit the server's cap and stopped. Said out loud
|
|
778
|
+
* in the tree rather than swallowed: a file tree that silently omits files is
|
|
779
|
+
* worse than no file tree. */
|
|
780
|
+
export interface ProjectFiles {
|
|
781
|
+
files: ProjectFile[];
|
|
782
|
+
truncated?: boolean;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Every editable file in the project. The route has always existed — the
|
|
786
|
+
* motion chrome's drawer used it — and this is the shot chrome finally asking.
|
|
787
|
+
* Editable extensions only, no dotfiles, no `node_modules`, no `.studio`. */
|
|
788
|
+
export function listFiles(): Promise<ProjectFiles> {
|
|
789
|
+
return request("/api/files");
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function readFile(rel: string): Promise<SourceFile> {
|
|
793
|
+
return request(`/api/files/${encodeURIComponent(rel)}`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** `baseHash` makes the write conflict-checked: a 409 lands in `ApiError.status`
|
|
797
|
+
* rather than overwriting whatever arrived under the buffer. */
|
|
798
|
+
export function writeFile(
|
|
799
|
+
rel: string,
|
|
800
|
+
content: string,
|
|
801
|
+
baseHash?: string,
|
|
802
|
+
): Promise<SourceFileSaved> {
|
|
803
|
+
return request(`/api/files/${encodeURIComponent(rel)}`, {
|
|
804
|
+
method: "PUT",
|
|
805
|
+
body: JSON.stringify(baseHash !== undefined ? { content, baseHash } : { content }),
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** Create a new file. A path that exists answers 409 rather than overwriting. */
|
|
810
|
+
export function createFile(rel: string, content: string): Promise<SourceFileSaved> {
|
|
811
|
+
return request(`/api/files/${encodeURIComponent(rel)}`, { method: "POST", body: JSON.stringify({ content }) });
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** Typecheck a file. `content` checks an UNSAVED buffer instead of disk, which
|
|
815
|
+
* is what puts a red gutter under code the author has not committed yet.
|
|
816
|
+
* Hosted, the code host's extractor answers it, in the same shape. */
|
|
817
|
+
export function fileDiagnostics(
|
|
818
|
+
rel: string,
|
|
819
|
+
content?: string,
|
|
820
|
+
): Promise<{ diagnostics: CodeDiagnostic[] }> {
|
|
821
|
+
const host = dailiesHost();
|
|
822
|
+
if (host !== undefined) return viaHost(() => host.diagnostics(rel, content));
|
|
823
|
+
return request("/api/diagnostics", {
|
|
824
|
+
method: "POST",
|
|
825
|
+
body: JSON.stringify(content !== undefined ? { path: rel, content } : { path: rel }),
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
173
829
|
// ---- the asset store ---------------------------------------------------------
|
|
174
830
|
|
|
175
|
-
/** What
|
|
176
|
-
*
|
|
831
|
+
/** What the Project-scoped asset POST answers with. `path` is the BARE
|
|
832
|
+
* content-addressed filename; identical bytes always land on it, so a
|
|
833
|
+
* re-import is idempotent. */
|
|
177
834
|
export interface UploadedAsset {
|
|
178
835
|
path: string;
|
|
179
836
|
hash: string;
|
|
180
837
|
/** Absent for an extension the store does not recognise. */
|
|
181
838
|
kind?: string;
|
|
839
|
+
/** Canonical bytes after server-side conversion (not the upload's size). */
|
|
840
|
+
size?: number;
|
|
841
|
+
usedBytes?: number;
|
|
842
|
+
remainingBytes?: number;
|
|
843
|
+
/** A video's measurement, which the chrome writes onto the asset row
|
|
844
|
+
* (`mergeAssetMedia`) as `dailies media import` does, so one upload
|
|
845
|
+
* registers the same row on either host. Locally the route's own answer
|
|
846
|
+
* (its probe of the staged bytes, on a re-upload too); hosted, the
|
|
847
|
+
* browser's probe (`uploadAsset` below), since the Worker measures
|
|
848
|
+
* nothing. */
|
|
849
|
+
media?: MediaProbe;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export interface ProjectAssetUsage {
|
|
853
|
+
limitBytes: number;
|
|
854
|
+
usedBytes: number;
|
|
855
|
+
reservedBytes: number;
|
|
856
|
+
remainingBytes: number;
|
|
857
|
+
assets: ReadonlyArray<{
|
|
858
|
+
path: string;
|
|
859
|
+
hash: string;
|
|
860
|
+
kind?: string;
|
|
861
|
+
size: number;
|
|
862
|
+
}>;
|
|
863
|
+
/** Registry rows whose file is gone from local storage (the local server's
|
|
864
|
+
* `ProjectAssetQuota`). Absent hosted, where stored media cannot go missing. */
|
|
865
|
+
missing?: ReadonlyArray<{ id: string; path: string }>;
|
|
182
866
|
}
|
|
183
867
|
|
|
184
868
|
/**
|
|
185
|
-
* Upload one file into the
|
|
186
|
-
* uses; the shot lane is served by the same server, so it is the same route).
|
|
869
|
+
* Upload one file into the open Project's quota-accounted asset store.
|
|
187
870
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
871
|
+
* HOSTED, the page does the local route's media work around the POST (browser
|
|
872
|
+
* host media/host.ts): audio is canonicalised first (the Worker stores bytes
|
|
873
|
+
* as they arrive and refuses any other audio), and after it a video is
|
|
874
|
+
* measured and every upload's sidecars are derived. Locally the route does
|
|
875
|
+
* all of that itself.
|
|
876
|
+
*
|
|
877
|
+
* `remainingBytes` is the meter's number (`assetUsage`), when the caller has
|
|
878
|
+
* one. Hosted, a file over it is refused before any byte is sent, as the
|
|
879
|
+
* Worker reserves the bytes before it dedupes; the server's check still
|
|
880
|
+
* stands behind this one.
|
|
881
|
+
*/
|
|
882
|
+
export async function uploadAsset(project: string, file: File, remainingBytes?: number): Promise<UploadedAsset> {
|
|
883
|
+
const host = dailiesHost();
|
|
884
|
+
if (host === undefined) {
|
|
885
|
+
// Locally only the whole allowance: the route answers bytes the Project
|
|
886
|
+
// already holds before it checks the space left, and audio is judged as
|
|
887
|
+
// the WAV only the route makes.
|
|
888
|
+
if (!file.type.startsWith("audio/")) await refuseOverSpace(project, file, undefined);
|
|
889
|
+
return postAsset(project, file);
|
|
890
|
+
}
|
|
891
|
+
const media = host.media;
|
|
892
|
+
const prepared = await media.prepareUpload(file);
|
|
893
|
+
// Hosted, the browser made the WAV, so audio is judged as it will be stored.
|
|
894
|
+
await refuseOverSpace(project, prepared, remainingBytes, prepared === file ? undefined : "wav");
|
|
895
|
+
const uploaded = await postAsset(project, prepared, await sha256Hex(prepared));
|
|
896
|
+
const probe = await media.uploaded(uploaded.path, prepared);
|
|
897
|
+
return probe === null ? uploaded : { ...uploaded, media: probe };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* The POST itself. The filename rides a URI-ENCODED header because HTTP
|
|
902
|
+
* headers are latin-1 and user filenames are not, and the BODY is the raw
|
|
903
|
+
* bytes — this is the one write in this module that is not JSON, so it
|
|
904
|
+
* overrides `request`'s content-type rather than letting it claim the bytes
|
|
905
|
+
* are JSON.
|
|
192
906
|
*/
|
|
193
|
-
|
|
194
|
-
return request(
|
|
907
|
+
function postAsset(project: string, file: File, sha256?: string): Promise<UploadedAsset> {
|
|
908
|
+
return request(`/api/shots/${encodeURIComponent(project)}/assets`, {
|
|
195
909
|
method: "POST",
|
|
196
910
|
body: file,
|
|
197
911
|
headers: {
|
|
198
912
|
"content-type": "application/octet-stream",
|
|
199
913
|
"x-studio-filename": encodeURIComponent(file.name),
|
|
914
|
+
...(sha256 === undefined ? {} : { [CONTENT_SHA256_HEADER]: sha256 }),
|
|
200
915
|
},
|
|
201
916
|
});
|
|
202
917
|
}
|
|
203
918
|
|
|
919
|
+
/**
|
|
920
|
+
* Refuse a file the Project cannot hold before streaming a doomed body, in the
|
|
921
|
+
* server's own words (`quotaMessage`). Over the whole allowance needs no
|
|
922
|
+
* number from anyone. Over `hint` asks the server first: the meter can be
|
|
923
|
+
* stale (a save may have freed space since), and a refusal is only as good as
|
|
924
|
+
* the number it quotes. If that read fails, the server judges the upload.
|
|
925
|
+
*/
|
|
926
|
+
async function refuseOverSpace(
|
|
927
|
+
project: string,
|
|
928
|
+
file: File,
|
|
929
|
+
hint: number | undefined,
|
|
930
|
+
storedAs?: "wav",
|
|
931
|
+
): Promise<void> {
|
|
932
|
+
const wav = storedAs === undefined ? {} : { storedAs };
|
|
933
|
+
if (file.size > PROJECT_MEDIA_LIMIT_BYTES) {
|
|
934
|
+
const whole = `This file is ${formatBytes(file.size)}, over the ${formatBytes(PROJECT_MEDIA_LIMIT_BYTES)} media allowance of a whole Project.`;
|
|
935
|
+
throw new ApiError(
|
|
936
|
+
413,
|
|
937
|
+
storedAs === undefined ? whole : `Audio is stored uncompressed (48 kHz WAV). ${whole}`,
|
|
938
|
+
undefined,
|
|
939
|
+
{ code: "project_quota_exceeded", limitBytes: PROJECT_MEDIA_LIMIT_BYTES, requestedBytes: file.size, ...wav },
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (hint === undefined || file.size <= hint) return;
|
|
943
|
+
let usage: ProjectAssetUsage;
|
|
944
|
+
try {
|
|
945
|
+
usage = await projectAssetUsage(project);
|
|
946
|
+
} catch {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (typeof usage.remainingBytes !== "number" || file.size <= usage.remainingBytes) return;
|
|
950
|
+
const details = {
|
|
951
|
+
code: "project_quota_exceeded",
|
|
952
|
+
limitBytes: usage.limitBytes,
|
|
953
|
+
usedBytes: usage.usedBytes,
|
|
954
|
+
requestedBytes: file.size,
|
|
955
|
+
remainingBytes: usage.remainingBytes,
|
|
956
|
+
...wav,
|
|
957
|
+
};
|
|
958
|
+
throw new ApiError(413, quotaMessage(details), undefined, details);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* What a hosted upload says its bytes hash to (the Worker's
|
|
963
|
+
* `WORKER_HEADERS.contentSha256`): the Worker hashes the body anyway and
|
|
964
|
+
* refuses a mismatch, and skips storing bytes the workspace already holds
|
|
965
|
+
* (a re-upload, the same file in another Project). Hosted only; the local
|
|
966
|
+
* route stores every upload.
|
|
967
|
+
*/
|
|
968
|
+
export const CONTENT_SHA256_HEADER = "x-studio-content-sha256";
|
|
969
|
+
|
|
970
|
+
async function sha256Hex(file: Blob): Promise<string> {
|
|
971
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", await file.arrayBuffer()));
|
|
972
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Server-authoritative Project usage, including bytes reserved by concurrent
|
|
976
|
+
* uploads. Kept beside upload because both are one quota surface. */
|
|
977
|
+
export function projectAssetUsage(project: string): Promise<ProjectAssetUsage> {
|
|
978
|
+
return request(`/api/shots/${encodeURIComponent(project)}/assets`);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** The server sends the ceiling it enforced. Say THAT number back, rather than
|
|
982
|
+
* a literal that can drift away from the rule the refusal came from. */
|
|
983
|
+
function quotaMessage(details: Readonly<Record<string, unknown>>): string {
|
|
984
|
+
const remaining = typeof details["remainingBytes"] === "number" ? details["remainingBytes"] : 0;
|
|
985
|
+
const requested = typeof details["requestedBytes"] === "number" ? details["requestedBytes"] : null;
|
|
986
|
+
const limit = formatBytes(
|
|
987
|
+
typeof details["limitBytes"] === "number" ? details["limitBytes"] : PROJECT_MEDIA_LIMIT_BYTES,
|
|
988
|
+
);
|
|
989
|
+
const left = formatBytes(remaining);
|
|
990
|
+
const message =
|
|
991
|
+
requested === null
|
|
992
|
+
? `This Project has ${left} of its ${limit} media allowance left.`
|
|
993
|
+
: `This file needs ${formatBytes(requested)}, but this Project has ${left} of its ${limit} media allowance left.`;
|
|
994
|
+
// The server judged the stored WAV, not the file the user picked, so say why
|
|
995
|
+
// the number is bigger than their MP3.
|
|
996
|
+
return details["storedAs"] === "wav" ? `Audio is stored uncompressed (48 kHz WAV). ${message}` : message;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** A byte count as an author reads it. The one copy: the quota refusal, the
|
|
1000
|
+
* usage meter and every file row say the same number the same way. */
|
|
1001
|
+
export function formatBytes(bytes: number): string {
|
|
1002
|
+
const safe = Math.max(0, bytes);
|
|
1003
|
+
if (safe >= 1_000_000) return `${(safe / 1_000_000).toFixed(safe >= 10_000_000 ? 0 : 1)} MB`;
|
|
1004
|
+
if (safe >= 1_000) return `${Math.round(safe / 1_000)} KB`;
|
|
1005
|
+
return `${safe} B`;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
204
1008
|
/**
|
|
205
1009
|
* The stored bytes of an asset.
|
|
206
1010
|
*
|
|
@@ -210,6 +1014,293 @@ export function uploadAsset(file: File): Promise<UploadedAsset> {
|
|
|
210
1014
|
* and the stored one's can differ by a partial frame, and the clip has to be
|
|
211
1015
|
* timed against what will actually be mixed.
|
|
212
1016
|
*/
|
|
1017
|
+
/** One workspace font (store `WorkspaceFont`): uploaded once to the
|
|
1018
|
+
* workspace, offered in every Project's font list. Its id is the stored path
|
|
1019
|
+
* a Project copy of the same bytes gets, so a copy is found by
|
|
1020
|
+
* `asset.path === id`. The optional keys are what the file's name table
|
|
1021
|
+
* says, and are absent when it could not be read (a file uploaded before
|
|
1022
|
+
* migration 0033 keeps its file-name family and nothing else). */
|
|
1023
|
+
export interface WorkspaceFont {
|
|
1024
|
+
id: string;
|
|
1025
|
+
/** The name table's family (name ID 16, then 1), else the file stem. */
|
|
1026
|
+
family: string;
|
|
1027
|
+
filename: string;
|
|
1028
|
+
format: string;
|
|
1029
|
+
size: number;
|
|
1030
|
+
/** OS/2 usWeightClass, 1 to 1000. */
|
|
1031
|
+
weight?: number;
|
|
1032
|
+
italic?: boolean;
|
|
1033
|
+
/** A variable font's `wght` axis: [min, default, max]. */
|
|
1034
|
+
wghtAxis?: [number, number, number];
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** `GET /api/fonts` on both hosts. `canManage` is whether this person may
|
|
1038
|
+
* upload and remove (`fonts.manage`; always locally). */
|
|
1039
|
+
export interface WorkspaceFontList {
|
|
1040
|
+
fonts: WorkspaceFont[];
|
|
1041
|
+
canManage: boolean;
|
|
1042
|
+
maxFonts: number;
|
|
1043
|
+
limitBytes: number;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export function listWorkspaceFonts(): Promise<WorkspaceFontList> {
|
|
1047
|
+
return request("/api/fonts");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export const workspaceFontUrl = (id: string): string => `/studio/fonts/${encodeURIComponent(id)}`;
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Add a font file to the workspace's fonts (`POST /studio/fonts`, the font
|
|
1054
|
+
* picker's upload, on both hosts). `created` is false when the workspace
|
|
1055
|
+
* already held these bytes (a 200 answering that row). A refusal is an
|
|
1056
|
+
* `ApiError` carrying the server's message and `code` as they came
|
|
1057
|
+
* (`font_too_large`, `fonts_full`, `forbidden`, `rate_limited`...), with the
|
|
1058
|
+
* whole body in `details` (`limitBytes`, `maxFonts`, `retryAfter`).
|
|
1059
|
+
*/
|
|
1060
|
+
export async function uploadWorkspaceFont(file: File): Promise<{ font: WorkspaceFont; created: boolean }> {
|
|
1061
|
+
let status = 0;
|
|
1062
|
+
const body = await request<{ font: WorkspaceFont | null }>(
|
|
1063
|
+
"/studio/fonts",
|
|
1064
|
+
{
|
|
1065
|
+
method: "POST",
|
|
1066
|
+
body: file,
|
|
1067
|
+
headers: { "content-type": "application/octet-stream", "x-studio-filename": encodeURIComponent(file.name) },
|
|
1068
|
+
},
|
|
1069
|
+
false,
|
|
1070
|
+
(s) => {
|
|
1071
|
+
status = s;
|
|
1072
|
+
},
|
|
1073
|
+
);
|
|
1074
|
+
if (body.font === null) throw new ApiError(status, `${file.name} was stored, but the workspace could not read it back.`);
|
|
1075
|
+
return { font: body.font, created: status === 201 };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/** How a font download reports: bytes read so far, and the whole size when
|
|
1079
|
+
* the response says it (`Content-Length` on an unencoded body), else null. */
|
|
1080
|
+
export interface FontDownloadOptions {
|
|
1081
|
+
signal?: AbortSignal;
|
|
1082
|
+
onProgress?: (loaded: number, total: number | null) => void;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** A response body read whole, reporting as it streams. */
|
|
1086
|
+
async function readFontBody(res: Response, opts: FontDownloadOptions): Promise<Blob> {
|
|
1087
|
+
const reader = opts.onProgress === undefined ? undefined : res.body?.getReader();
|
|
1088
|
+
if (reader === undefined) return res.blob();
|
|
1089
|
+
const length = Number(res.headers.get("content-length"));
|
|
1090
|
+
const total = res.headers.has("content-encoding") || !(length > 0) ? null : length;
|
|
1091
|
+
const chunks: Uint8Array[] = [];
|
|
1092
|
+
let loaded = 0;
|
|
1093
|
+
opts.onProgress?.(0, total);
|
|
1094
|
+
for (;;) {
|
|
1095
|
+
const { done, value } = await reader.read();
|
|
1096
|
+
if (done) break;
|
|
1097
|
+
chunks.push(value);
|
|
1098
|
+
loaded += value.byteLength;
|
|
1099
|
+
opts.onProgress?.(loaded, total);
|
|
1100
|
+
}
|
|
1101
|
+
return new Blob(chunks);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** Who a saved video's page opens for (worker videos.ts). */
|
|
1105
|
+
export type VideoVisibility = "private" | "workspace" | "link";
|
|
1106
|
+
|
|
1107
|
+
export interface SaveVideoMeta {
|
|
1108
|
+
/** The Project's name (the store's `name`), whose export this is. */
|
|
1109
|
+
project: string;
|
|
1110
|
+
title: string;
|
|
1111
|
+
filename: string;
|
|
1112
|
+
visibility: VideoVisibility;
|
|
1113
|
+
/** The "Sound credits" block the export owes (v49), shown on its page. */
|
|
1114
|
+
credits?: string;
|
|
1115
|
+
durationMs: number;
|
|
1116
|
+
width: number;
|
|
1117
|
+
height: number;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Save an export to the workspace's Videos, hosted only, and answer its id
|
|
1122
|
+
* and page. In parts, at the size the server names: a video can be larger
|
|
1123
|
+
* than one request may carry.
|
|
1124
|
+
*/
|
|
1125
|
+
export async function saveVideo(blob: Blob, meta: SaveVideoMeta): Promise<{ id: string; url: string }> {
|
|
1126
|
+
const started = await request<{ id: string; partBytes: number; parts: number }>("/studio/videos", {
|
|
1127
|
+
method: "POST",
|
|
1128
|
+
body: JSON.stringify({ ...meta, size: blob.size }),
|
|
1129
|
+
});
|
|
1130
|
+
const base = `/studio/videos/${encodeURIComponent(started.id)}`;
|
|
1131
|
+
const parts: Array<{ partNumber: number; etag: string }> = [];
|
|
1132
|
+
for (let n = 1; n <= started.parts; n++) {
|
|
1133
|
+
parts.push(
|
|
1134
|
+
await request(`${base}/parts/${n}`, {
|
|
1135
|
+
method: "PUT",
|
|
1136
|
+
body: blob.slice((n - 1) * started.partBytes, n * started.partBytes),
|
|
1137
|
+
headers: { "content-type": "application/octet-stream" },
|
|
1138
|
+
}),
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
const done = await request<{ url: string }>(`${base}/complete`, { method: "POST", body: JSON.stringify({ parts }) });
|
|
1142
|
+
return { id: started.id, url: done.url };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** The font's bytes as a File named like the upload, ready for `uploadAsset`. */
|
|
1146
|
+
export async function fetchWorkspaceFont(font: WorkspaceFont, opts: FontDownloadOptions = {}): Promise<File> {
|
|
1147
|
+
const url = workspaceFontUrl(font.id);
|
|
1148
|
+
const res = await fetch(url, opts.signal === undefined ? undefined : { signal: opts.signal });
|
|
1149
|
+
if (!res.ok) throw new ApiError(res.status, `${font.family} is no longer in this workspace's fonts.`);
|
|
1150
|
+
return new File([await readFontBody(res, opts)], font.filename, { type: res.headers.get("content-type") ?? "" });
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/** A font file from a url (a Google font on jsDelivr, the chrome's bundled
|
|
1154
|
+
* Inter) as a File named `name`, ready for `uploadAsset`. */
|
|
1155
|
+
export async function fetchFontFile(url: string, name: string, opts: FontDownloadOptions = {}): Promise<File> {
|
|
1156
|
+
const res = await fetch(url, opts.signal === undefined ? undefined : { signal: opts.signal });
|
|
1157
|
+
if (!res.ok) throw new ApiError(res.status, `${name} could not be fetched (${res.status}).`);
|
|
1158
|
+
return new File([await readFontBody(res, opts)], name, { type: "font/woff2" });
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// ---- the generate drawer (Sound mode) ----------------------------------------
|
|
1162
|
+
//
|
|
1163
|
+
// The wire of `/studio/audio/generate`, on both hosts. The shapes are
|
|
1164
|
+
// `GenerationStatus` and `GenerationTake` in `@dailies-studio/store`
|
|
1165
|
+
// (audioGeneration.ts), restated because the chrome does not depend on the
|
|
1166
|
+
// store package.
|
|
1167
|
+
|
|
1168
|
+
export type GenerationKind = "sfx" | "music" | "voice";
|
|
1169
|
+
|
|
1170
|
+
export interface GenerationTake {
|
|
1171
|
+
id: string;
|
|
1172
|
+
kind: GenerationKind;
|
|
1173
|
+
prompt: string;
|
|
1174
|
+
seconds: number | null;
|
|
1175
|
+
voice: string | null;
|
|
1176
|
+
provider: string;
|
|
1177
|
+
model: string;
|
|
1178
|
+
createdAt: number;
|
|
1179
|
+
name: string;
|
|
1180
|
+
/** Where the bytes are served, same-origin. */
|
|
1181
|
+
url: string;
|
|
1182
|
+
/** Set when the take is already a stored asset (local). */
|
|
1183
|
+
path?: string;
|
|
1184
|
+
hash?: string;
|
|
1185
|
+
/** In the sound history past its waiting day. */
|
|
1186
|
+
kept?: boolean;
|
|
1187
|
+
/** Music composed to a Project's sections: it lines up only from frame 0. */
|
|
1188
|
+
scored?: boolean;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/** One page of the sound history, newest first (store `GenerationHistory`).
|
|
1192
|
+
* A page can be short, even empty, and still have a `next`. */
|
|
1193
|
+
export interface GenerationHistory {
|
|
1194
|
+
takes: GenerationTake[];
|
|
1195
|
+
next: string | null;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export interface GenerationStatus {
|
|
1199
|
+
host: "local" | "hosted";
|
|
1200
|
+
kinds: Record<GenerationKind, { ok: boolean; reason?: string }>;
|
|
1201
|
+
voices: ReadonlyArray<{ id: string; name: string; description: string }>;
|
|
1202
|
+
caps: {
|
|
1203
|
+
promptMaxChars: number;
|
|
1204
|
+
sfx: { minSeconds: number; maxSeconds: number; defaultSeconds: number };
|
|
1205
|
+
music: { minSeconds: number; maxSeconds: number; defaultSeconds: number };
|
|
1206
|
+
voice: { maxChars: number };
|
|
1207
|
+
};
|
|
1208
|
+
/** Hosted, with no ElevenLabs key the workspace can use: "add" when this
|
|
1209
|
+
* member may add one (Settings, AI keys), "ask" when an Owner or Admin must. */
|
|
1210
|
+
needsKey?: "add" | "ask";
|
|
1211
|
+
recent: GenerationTake[];
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export interface GenerationBody {
|
|
1215
|
+
project: string;
|
|
1216
|
+
kind: GenerationKind;
|
|
1217
|
+
prompt: string;
|
|
1218
|
+
seconds?: number;
|
|
1219
|
+
voice?: string;
|
|
1220
|
+
instrumental?: boolean;
|
|
1221
|
+
loop?: boolean;
|
|
1222
|
+
/** Music scored to the Project: its sections, in order (`scoreSectionsOf`). */
|
|
1223
|
+
sections?: Array<{ name: string; durationMs: number }>;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export const AUDIO_GENERATE_ROUTE = "/studio/audio/generate";
|
|
1227
|
+
|
|
1228
|
+
export function generationStatus(project: string): Promise<GenerationStatus> {
|
|
1229
|
+
return request(`${AUDIO_GENERATE_ROUTE}?project=${encodeURIComponent(project)}`);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export function generateAudioTake(body: GenerationBody): Promise<{ take: GenerationTake }> {
|
|
1233
|
+
return request(AUDIO_GENERATE_ROUTE, { method: "POST", body: JSON.stringify(body) });
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/** The sound history: every take this member may see that is kept or still
|
|
1237
|
+
* in its day, newest first; `before` asks for the page after a `next`. */
|
|
1238
|
+
export function generationHistory(before?: string): Promise<GenerationHistory> {
|
|
1239
|
+
return request(`${AUDIO_GENERATE_ROUTE}/history${before === undefined ? "" : `?before=${encodeURIComponent(before)}`}`);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/** Keep a take in the history past its day: it went into `project` (by its
|
|
1243
|
+
* storage name), which lets a member keep another's take they added. */
|
|
1244
|
+
export function keepGeneration(id: string, project?: string): Promise<{ kept: boolean }> {
|
|
1245
|
+
return request(`${AUDIO_GENERATE_ROUTE}/${encodeURIComponent(id)}/keep`, {
|
|
1246
|
+
method: "POST",
|
|
1247
|
+
body: JSON.stringify(project === undefined ? {} : { project }),
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** Delete a take from the history. Locally its file stays, for any Project
|
|
1252
|
+
* that uses it; hosted, its waiting-room bytes go. */
|
|
1253
|
+
export function deleteGeneration(id: string): Promise<{ deleted: boolean }> {
|
|
1254
|
+
return request(`${AUDIO_GENERATE_ROUTE}/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/** A hosted take's bytes as a File, ready for `importAudio`. */
|
|
1258
|
+
export async function fetchGenerationFile(take: GenerationTake): Promise<File> {
|
|
1259
|
+
const res = await fetch(take.url);
|
|
1260
|
+
if (!res.ok) throw new ApiError(res.status, "That take has expired. Generate it again.");
|
|
1261
|
+
return new File([await res.blob()], `${take.name}.mp3`, { type: res.headers.get("content-type") ?? "audio/mpeg" });
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// ---- stock sounds (the Library's Stock sounds) -------------------------------
|
|
1265
|
+
//
|
|
1266
|
+
// The wire of `/api/openverse`, on both hosts. `OpenverseTake` is the store's
|
|
1267
|
+
// (openverse.ts), restated because the chrome does not depend on the store
|
|
1268
|
+
// package. The search itself never comes here: the Library searches Openverse
|
|
1269
|
+
// from this browser (`panels/library/stockSearch.ts`).
|
|
1270
|
+
|
|
1271
|
+
export const OPENVERSE_ROUTE = "/api/openverse";
|
|
1272
|
+
|
|
1273
|
+
export interface OpenverseTake {
|
|
1274
|
+
/** The provenance to record: the host's own read of the row. */
|
|
1275
|
+
source: OpenverseSource3D;
|
|
1276
|
+
/** Same-origin: where the file is read. */
|
|
1277
|
+
file: string;
|
|
1278
|
+
filename: string;
|
|
1279
|
+
durationMs: number;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** Whether this host can add a stock sound: locally always, hosted once the
|
|
1283
|
+
* deployment has a registered Openverse app. */
|
|
1284
|
+
export function openverseStatus(): Promise<{ imports: boolean }> {
|
|
1285
|
+
return request(OPENVERSE_ROUTE);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/** The host reads the row itself and answers the provenance to record and
|
|
1289
|
+
* where to read the file. */
|
|
1290
|
+
export function openverseTake(input: { id: string; project: string }): Promise<OpenverseTake> {
|
|
1291
|
+
return request(`${OPENVERSE_ROUTE}/take`, { method: "POST", body: JSON.stringify(input) });
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** A take's bytes as a File, ready for the audio upload lane. */
|
|
1295
|
+
export async function fetchOpenverseFile(take: OpenverseTake): Promise<File> {
|
|
1296
|
+
const res = await fetch(take.file);
|
|
1297
|
+
if (!res.ok) {
|
|
1298
|
+
const body = (await res.json().catch(() => null)) as { error?: unknown } | null;
|
|
1299
|
+
throw new ApiError(res.status, typeof body?.error === "string" ? body.error : `The sound could not be fetched (${res.status}).`);
|
|
1300
|
+
}
|
|
1301
|
+
return new File([await res.blob()], take.filename, { type: "audio/mpeg" });
|
|
1302
|
+
}
|
|
1303
|
+
|
|
213
1304
|
export async function fetchAssetBytes(path: string): Promise<ArrayBuffer> {
|
|
214
1305
|
const url = `/studio/assets/${encodeURIComponent(path)}`;
|
|
215
1306
|
const res = await fetch(url);
|
|
@@ -227,6 +1318,16 @@ export interface RenderJob {
|
|
|
227
1318
|
total: number;
|
|
228
1319
|
out?: string;
|
|
229
1320
|
error?: string;
|
|
1321
|
+
/** Every `warning:` line the render emitted. Optional on the wire: a server
|
|
1322
|
+
* older than this field simply omits it, and the dialog renders nothing. */
|
|
1323
|
+
warnings?: string[];
|
|
1324
|
+
/** A failed render's last output lines, `[page]` errors included. */
|
|
1325
|
+
log?: string[];
|
|
1326
|
+
/** A failed render's forensics folder, when one was written. */
|
|
1327
|
+
diagnostics?: string;
|
|
1328
|
+
/** What the render is doing before its first frame, such as downloading
|
|
1329
|
+
* the browser renderer. Cleared once frames start. */
|
|
1330
|
+
note?: string;
|
|
230
1331
|
elapsedMs: number;
|
|
231
1332
|
}
|
|
232
1333
|
|
|
@@ -235,6 +1336,9 @@ export interface RenderRequest {
|
|
|
235
1336
|
quality: ExportQualityId;
|
|
236
1337
|
/** Absent = the whole timeline. */
|
|
237
1338
|
sceneId?: string;
|
|
1339
|
+
/** Master frames [start, end) of the whole timeline, for the work area.
|
|
1340
|
+
* Never sent with `sceneId`. */
|
|
1341
|
+
frameRange?: { start: number; end: number };
|
|
238
1342
|
/** `false` renders a SILENT file even though the document has clips. Absent
|
|
239
1343
|
* = the server's default, which is to mux whatever `doc.audio` carries. */
|
|
240
1344
|
audio?: boolean;
|
|
@@ -270,6 +1374,414 @@ export function cancelRender(name: string): Promise<{ cancelled: boolean }> {
|
|
|
270
1374
|
return request(`/api/shots/${encodeURIComponent(name)}/render`, { method: "DELETE" });
|
|
271
1375
|
}
|
|
272
1376
|
|
|
1377
|
+
// ---- comments (packages/store/src/commentRoutes.ts) ------------------------
|
|
1378
|
+
|
|
1379
|
+
/** A pin on a storyboard Frame (a still). */
|
|
1380
|
+
export interface CommentFrameAnchor {
|
|
1381
|
+
kind: "frame";
|
|
1382
|
+
/** The Shot the Frame sat in when the thread was made. */
|
|
1383
|
+
sceneId: string;
|
|
1384
|
+
stillId: string;
|
|
1385
|
+
/** 0..1 across the picture, left to right. */
|
|
1386
|
+
u: number;
|
|
1387
|
+
/** 0..1 down the picture, top to bottom. */
|
|
1388
|
+
v: number;
|
|
1389
|
+
layerId: string | null;
|
|
1390
|
+
frame: number;
|
|
1391
|
+
frameLabel: string | null;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/** A thread on a board card (migration 0029): a planned shot (`partId`) or
|
|
1395
|
+
* a reference (`referenceId`), exactly one of the two. `sceneId` is the Shot
|
|
1396
|
+
* the card pointed at when written, or null. `stillId` is never set: it is
|
|
1397
|
+
* here so "does this thread sit on still X" reads on any anchor. */
|
|
1398
|
+
export interface CommentCardAnchor {
|
|
1399
|
+
kind: "card";
|
|
1400
|
+
partId: string | null;
|
|
1401
|
+
referenceId: string | null;
|
|
1402
|
+
sceneId: string | null;
|
|
1403
|
+
stillId?: never;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** A thread on a spot in the Studio (migration 0029): a Shot at a Shot-local
|
|
1407
|
+
* `frame`, the layer under the click (or none), the point as 0..1 of the
|
|
1408
|
+
* camera frame. `stillId` as on a card. */
|
|
1409
|
+
export interface CommentStudioAnchor {
|
|
1410
|
+
kind: "studio";
|
|
1411
|
+
sceneId: string;
|
|
1412
|
+
frame: number;
|
|
1413
|
+
layerId: string | null;
|
|
1414
|
+
u: number;
|
|
1415
|
+
v: number;
|
|
1416
|
+
stillId?: never;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/** Where a thread is pinned, always read back with its `kind`. */
|
|
1420
|
+
export type CommentAnchor = CommentFrameAnchor | CommentCardAnchor | CommentStudioAnchor;
|
|
1421
|
+
export type CommentAnchorKind = CommentAnchor["kind"];
|
|
1422
|
+
|
|
1423
|
+
/** One thread linked to another (0029): a board thread and a Studio thread,
|
|
1424
|
+
* seen from either side. `kind` is the other thread's anchor kind. */
|
|
1425
|
+
export interface CommentLink {
|
|
1426
|
+
threadId: string;
|
|
1427
|
+
kind: CommentAnchorKind;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
export interface CommentPerson {
|
|
1431
|
+
id: string;
|
|
1432
|
+
login: string;
|
|
1433
|
+
displayName: string | null;
|
|
1434
|
+
avatarKey: string | null;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
export interface CommentMessage {
|
|
1438
|
+
id: string;
|
|
1439
|
+
threadId: string;
|
|
1440
|
+
authorId: string;
|
|
1441
|
+
authorLabel: string;
|
|
1442
|
+
authorAvatarKey: string | null;
|
|
1443
|
+
/** Mentions are inline `<@usr-...>` tokens. Empty when deleted. */
|
|
1444
|
+
body: string;
|
|
1445
|
+
viaAgent: boolean;
|
|
1446
|
+
createdAt: number;
|
|
1447
|
+
editedAt: number | null;
|
|
1448
|
+
deleted: boolean;
|
|
1449
|
+
can: { edit: boolean; delete: boolean };
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
export interface CommentThread {
|
|
1453
|
+
id: string;
|
|
1454
|
+
projectId: string;
|
|
1455
|
+
anchor: CommentAnchor;
|
|
1456
|
+
/** The threads linked to this one, in the other place. */
|
|
1457
|
+
links: CommentLink[];
|
|
1458
|
+
status: "open" | "resolved";
|
|
1459
|
+
createdBy: string;
|
|
1460
|
+
assignee: CommentPerson | null;
|
|
1461
|
+
resolvedBy: string | null;
|
|
1462
|
+
resolvedAt: number | null;
|
|
1463
|
+
messageCount: number;
|
|
1464
|
+
snapshotKey: string | null;
|
|
1465
|
+
createdAt: number;
|
|
1466
|
+
updatedAt: number;
|
|
1467
|
+
lastMessageAt: number;
|
|
1468
|
+
head: CommentMessage | null;
|
|
1469
|
+
viewer: { lastReadAt: number | null; following: boolean; unread: boolean };
|
|
1470
|
+
/** Whether the anchor is there, judged against the SAVED document. */
|
|
1471
|
+
orphan: CommentOrphan;
|
|
1472
|
+
can: { reply: boolean; resolve: boolean; assign: boolean; delete: boolean; snapshot: boolean };
|
|
1473
|
+
/** Oldest first; only on a single-thread read. */
|
|
1474
|
+
messages?: CommentMessage[];
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/** Null while the anchor is there: a Frame pin's Frame, a card's part or
|
|
1478
|
+
* reference, a Studio thread's Shot and layer. */
|
|
1479
|
+
export type CommentOrphan = "frame-removed" | "card-removed" | "layer-removed" | "shot-removed" | null;
|
|
1480
|
+
|
|
1481
|
+
export interface MentionablePerson extends CommentPerson {
|
|
1482
|
+
guest: boolean;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
export interface Mentionable {
|
|
1486
|
+
/** People who can open the Project, with their role on it. */
|
|
1487
|
+
people: (MentionablePerson & { role: string })[];
|
|
1488
|
+
/** Members who cannot open it yet: only for a caller who may share it. */
|
|
1489
|
+
others: MentionablePerson[];
|
|
1490
|
+
canShare: boolean;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
export interface InboxItem {
|
|
1494
|
+
id: number;
|
|
1495
|
+
kind: "mention" | "reply" | "assigned" | "resolved" | "reopened";
|
|
1496
|
+
projectId: string;
|
|
1497
|
+
/** The Project's store name, the editor's `?shot=`; null when unknown. */
|
|
1498
|
+
projectName: string | null;
|
|
1499
|
+
projectTitle: string | null;
|
|
1500
|
+
threadId: string;
|
|
1501
|
+
/** '' for a card written on no Shot. */
|
|
1502
|
+
sceneId: string;
|
|
1503
|
+
/** '' for a card or Studio thread. */
|
|
1504
|
+
stillId: string;
|
|
1505
|
+
frameLabel: string | null;
|
|
1506
|
+
/** Where the thread sits, so an item opens the right place. */
|
|
1507
|
+
anchorKind: CommentAnchorKind;
|
|
1508
|
+
messageId: string | null;
|
|
1509
|
+
excerpt: string;
|
|
1510
|
+
actorId: string;
|
|
1511
|
+
actorLabel: string;
|
|
1512
|
+
viaAgent: boolean;
|
|
1513
|
+
createdAt: number;
|
|
1514
|
+
readAt: number | null;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
export type ThreadFilter = {
|
|
1518
|
+
status?: "open" | "resolved";
|
|
1519
|
+
filter?: "mine" | "mentions";
|
|
1520
|
+
cursor?: string;
|
|
1521
|
+
kind?: CommentAnchorKind;
|
|
1522
|
+
partId?: string;
|
|
1523
|
+
referenceId?: string;
|
|
1524
|
+
sceneId?: string;
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
const query = (params: Record<string, string | number | undefined>): string =>
|
|
1528
|
+
new URLSearchParams(
|
|
1529
|
+
Object.entries(params).flatMap(([key, value]) => (value === undefined ? [] : [[key, String(value)]])),
|
|
1530
|
+
).toString();
|
|
1531
|
+
|
|
1532
|
+
const threadPath = (id: string): string => `/api/comments/${encodeURIComponent(id)}`;
|
|
1533
|
+
|
|
1534
|
+
export function listThreads(
|
|
1535
|
+
projectId: string,
|
|
1536
|
+
filter: ThreadFilter = {},
|
|
1537
|
+
): Promise<{ threads: CommentThread[]; nextCursor?: string }> {
|
|
1538
|
+
return request(`/api/comments?${query({ project: projectId, limit: 50, ...filter })}`);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
export async function getThread(id: string): Promise<CommentThread> {
|
|
1542
|
+
return (await request<{ thread: CommentThread }>(threadPath(id))).thread;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
export async function createThread(projectId: string, anchor: CommentAnchor, body: string): Promise<CommentThread> {
|
|
1546
|
+
const res = await request<{ thread: CommentThread }>("/api/comments", {
|
|
1547
|
+
method: "POST",
|
|
1548
|
+
body: JSON.stringify({ projectId, anchor, body }),
|
|
1549
|
+
});
|
|
1550
|
+
return res.thread;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
export function replyThread(id: string, body: string): Promise<{ message: CommentMessage; thread: CommentThread }> {
|
|
1554
|
+
return request(`${threadPath(id)}/messages`, { method: "POST", body: JSON.stringify({ body }) });
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
export async function patchThread(
|
|
1558
|
+
id: string,
|
|
1559
|
+
patch: { status?: "open" | "resolved"; assigneeId?: string | null },
|
|
1560
|
+
): Promise<CommentThread> {
|
|
1561
|
+
return (await request<{ thread: CommentThread }>(threadPath(id), { method: "PATCH", body: JSON.stringify(patch) })).thread;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/** Link a board thread and a Studio thread of one Project. Idempotent.
|
|
1565
|
+
* Answers this thread with its links. */
|
|
1566
|
+
export async function linkThread(id: string, otherId: string): Promise<CommentThread> {
|
|
1567
|
+
const res = await request<{ thread: CommentThread }>(`${threadPath(id)}/links`, {
|
|
1568
|
+
method: "POST",
|
|
1569
|
+
body: JSON.stringify({ threadId: otherId }),
|
|
1570
|
+
});
|
|
1571
|
+
return res.thread;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** Undo `linkThread`. Idempotent. Answers this thread with its links. */
|
|
1575
|
+
export async function unlinkThread(id: string, otherId: string): Promise<CommentThread> {
|
|
1576
|
+
const res = await request<{ thread: CommentThread }>(`${threadPath(id)}/links/${encodeURIComponent(otherId)}`, {
|
|
1577
|
+
method: "DELETE",
|
|
1578
|
+
});
|
|
1579
|
+
return res.thread;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
export async function deleteThread(id: string): Promise<void> {
|
|
1583
|
+
await request(threadPath(id), { method: "DELETE" });
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
export async function editMessage(threadId: string, messageId: string, body: string): Promise<CommentMessage | null> {
|
|
1587
|
+
const res = await request<{ message: CommentMessage | null }>(
|
|
1588
|
+
`${threadPath(threadId)}/messages/${encodeURIComponent(messageId)}`,
|
|
1589
|
+
{ method: "PATCH", body: JSON.stringify({ body }) },
|
|
1590
|
+
);
|
|
1591
|
+
return res.message;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
export async function deleteMessage(threadId: string, messageId: string): Promise<void> {
|
|
1595
|
+
await request(`${threadPath(threadId)}/messages/${encodeURIComponent(messageId)}`, { method: "DELETE" });
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
export async function markThreadRead(id: string): Promise<void> {
|
|
1599
|
+
await request(`${threadPath(id)}/read`, { method: "POST" });
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
export function mentionable(projectId: string, q: string): Promise<Mentionable> {
|
|
1603
|
+
return request(`/api/comments/mentionable?${query({ project: projectId, q: q === "" ? undefined : q, limit: 8 })}`);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/** One person who can open a Project (the workspace's people route). */
|
|
1607
|
+
export interface ProjectPerson {
|
|
1608
|
+
userId: string;
|
|
1609
|
+
login: string;
|
|
1610
|
+
name: string;
|
|
1611
|
+
avatarUrl: string | null;
|
|
1612
|
+
isYou: boolean;
|
|
1613
|
+
projectRole: string;
|
|
1614
|
+
guest: boolean;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/** Who can open a Project: names and avatars for the presence faces. Hosted
|
|
1618
|
+
* only. */
|
|
1619
|
+
export function projectPeople(name: string): Promise<{ people: ProjectPerson[] }> {
|
|
1620
|
+
return request(`/api/projects/shot/${encodeURIComponent(name)}/people`);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
export function inbox(unreadOnly: boolean): Promise<{ items: InboxItem[]; nextCursor?: string; unread: number }> {
|
|
1624
|
+
return request(`/api/inbox?${query({ unread: unreadOnly ? 1 : undefined, limit: 30 })}`);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
export async function markInboxRead(ids?: readonly number[]): Promise<void> {
|
|
1628
|
+
await request("/api/inbox/read", { method: "POST", body: JSON.stringify(ids === undefined ? {} : { ids }) });
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/** Keep the picture the author commented on (webp, png or jpeg, at most
|
|
1632
|
+
* 256 KB): its author, once. A removed Frame's thread shows it. */
|
|
1633
|
+
export async function putSnapshot(id: string, picture: Blob): Promise<void> {
|
|
1634
|
+
await request(`${threadPath(id)}/snapshot`, {
|
|
1635
|
+
method: "PUT",
|
|
1636
|
+
body: picture,
|
|
1637
|
+
headers: { "content-type": picture.type },
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/** The picture a thread keeps, for an `<img src>`. */
|
|
1642
|
+
export const snapshotUrl = (id: string): string => `${threadPath(id)}/snapshot`;
|
|
1643
|
+
|
|
1644
|
+
/** Ask the Project's admins for Editor. Always 202, so nothing leaks about
|
|
1645
|
+
* whether a request was filed (packages/worker/src/requests.ts). */
|
|
1646
|
+
export async function requestEditAccess(projectId: string): Promise<void> {
|
|
1647
|
+
await request("/api/workspace/requests", { method: "POST", body: JSON.stringify({ projectId, role: "editor" }) });
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// ---- the workspace component store (packages/store/src/workspaceComponents.ts) --
|
|
1651
|
+
//
|
|
1652
|
+
// Restated here, as the comment shapes above are: the chrome cannot import the
|
|
1653
|
+
// store package. Keep these in step with that file.
|
|
1654
|
+
|
|
1655
|
+
/** Where a component lives: a definition in a Project, or a code module. */
|
|
1656
|
+
export type WorkspaceComponentHome = { projectId: string; def: string } | { component: string };
|
|
1657
|
+
|
|
1658
|
+
export interface WorkspaceComponent {
|
|
1659
|
+
id: string;
|
|
1660
|
+
kind: "studio" | "code";
|
|
1661
|
+
home: WorkspaceComponentHome;
|
|
1662
|
+
name: string;
|
|
1663
|
+
slug: string;
|
|
1664
|
+
/** The latest version's number and contentHash. */
|
|
1665
|
+
version: number;
|
|
1666
|
+
contentHash: string;
|
|
1667
|
+
createdAt: number;
|
|
1668
|
+
updatedAt: number;
|
|
1669
|
+
createdBy: string;
|
|
1670
|
+
/** A Studio-made component's home Project name, as the list answers it. */
|
|
1671
|
+
homeName?: string | null;
|
|
1672
|
+
/** In the workspace's library (`setWorkspaceComponentLibrary`): every
|
|
1673
|
+
* Project lists it under Your library. */
|
|
1674
|
+
library?: true;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/** A code file a live Project lists as made for it or uses, by module path
|
|
1678
|
+
* (`src/Gem.tsx`). `projectId` is null for a Project the caller may not
|
|
1679
|
+
* view: the file still belongs to some Project, just not one it may name. */
|
|
1680
|
+
export interface ComponentFileHome {
|
|
1681
|
+
path: string;
|
|
1682
|
+
projectId: string | null;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/** One version as the History list shows it. */
|
|
1686
|
+
export interface WorkspaceComponentVersionInfo {
|
|
1687
|
+
version: number;
|
|
1688
|
+
contentHash: string;
|
|
1689
|
+
name: string;
|
|
1690
|
+
/** The exposed prop keys (studio) or the contract keys (code), in order. */
|
|
1691
|
+
propKeys: string[];
|
|
1692
|
+
mediaHashes: string[];
|
|
1693
|
+
createdAt: number;
|
|
1694
|
+
createdBy: string;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
export interface StudioComponentPayload {
|
|
1698
|
+
kind: "studio";
|
|
1699
|
+
def: ComponentDef3D;
|
|
1700
|
+
nested: Record<string, ComponentDef3D>;
|
|
1701
|
+
assets: Record<string, ShotAsset>;
|
|
1702
|
+
materials: Record<string, MaterialGraph3D>;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
export interface CodeComponentPayload {
|
|
1706
|
+
kind: "code";
|
|
1707
|
+
files: { path: string; content: string }[];
|
|
1708
|
+
contract: unknown;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/** A version with its payload: the view and restore read. */
|
|
1712
|
+
export interface WorkspaceComponentVersion extends WorkspaceComponentVersionInfo {
|
|
1713
|
+
payload: StudioComponentPayload | CodeComponentPayload;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** One row of the usage index. */
|
|
1717
|
+
export type IndexedComponentUse = ComponentUse & { projectId: string; projectName: string };
|
|
1718
|
+
|
|
1719
|
+
export interface RecordedComponentVersion {
|
|
1720
|
+
component: WorkspaceComponent;
|
|
1721
|
+
version: WorkspaceComponentVersionInfo;
|
|
1722
|
+
created: boolean;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const WORKSPACE_COMPONENTS = "/api/workspace-components";
|
|
1726
|
+
const workspaceComponentPath = (id: string): string => `${WORKSPACE_COMPONENTS}/${encodeURIComponent(id)}`;
|
|
1727
|
+
|
|
1728
|
+
/** Every workspace component the caller may see, and which Project each code
|
|
1729
|
+
* file belongs to (`homes`), the index Your library reads. */
|
|
1730
|
+
export function listWorkspaceComponents(): Promise<{ components: WorkspaceComponent[]; homes: ComponentFileHome[] }> {
|
|
1731
|
+
return request(WORKSPACE_COMPONENTS);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/** Add a recorded component to the workspace's library, or take it out.
|
|
1735
|
+
* Answers the row as it now stands. */
|
|
1736
|
+
export async function setWorkspaceComponentLibrary(id: string, library: boolean): Promise<WorkspaceComponent> {
|
|
1737
|
+
return (
|
|
1738
|
+
await request<{ component: WorkspaceComponent }>(`${workspaceComponentPath(id)}/library`, {
|
|
1739
|
+
method: "PUT",
|
|
1740
|
+
body: JSON.stringify({ library }),
|
|
1741
|
+
})
|
|
1742
|
+
).component;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/** Record a version from the saved document (studio) or the registry (code).
|
|
1746
|
+
* The same content twice writes nothing (`created: false`). */
|
|
1747
|
+
export function recordWorkspaceComponent(
|
|
1748
|
+
body: { kind: "studio"; projectId: string; def: string } | { kind: "code"; component: string },
|
|
1749
|
+
): Promise<RecordedComponentVersion> {
|
|
1750
|
+
return request(WORKSPACE_COMPONENTS, { method: "POST", body: JSON.stringify(body) });
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/** Newest first. */
|
|
1754
|
+
export async function workspaceComponentVersions(id: string): Promise<WorkspaceComponentVersionInfo[]> {
|
|
1755
|
+
return (await request<{ versions: WorkspaceComponentVersionInfo[] }>(`${workspaceComponentPath(id)}/versions`)).versions;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
export async function workspaceComponentVersion(id: string, version: number): Promise<WorkspaceComponentVersion> {
|
|
1759
|
+
return (await request<{ version: WorkspaceComponentVersion }>(`${workspaceComponentPath(id)}/versions/${version}`)).version;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
export async function workspaceComponentUses(id: string): Promise<IndexedComponentUse[]> {
|
|
1763
|
+
return (await request<{ uses: IndexedComponentUse[] }>(`${workspaceComponentPath(id)}/uses`)).uses;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
let buildProbe = 0;
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Why a code component's module does not build now, or null when it does.
|
|
1770
|
+
* Hosted, the code host's compile answers (after a sync). Locally, the dev
|
|
1771
|
+
* server's transform: asking for the module is what compiles it, and a module
|
|
1772
|
+
* that fails answers non-2xx with no usable body (Vite's middleware mode hands
|
|
1773
|
+
* the error on without one), hence the plain sentence.
|
|
1774
|
+
*/
|
|
1775
|
+
export async function componentBuildError(rel: string): Promise<string | null> {
|
|
1776
|
+
const host = dailiesHost();
|
|
1777
|
+
if (host !== undefined) {
|
|
1778
|
+
if (host.buildErrors === undefined) return null;
|
|
1779
|
+
return (await host.buildErrors())[rel] ?? null;
|
|
1780
|
+
}
|
|
1781
|
+
const res = await fetch(`/${rel.split("/").map(encodeURIComponent).join("/")}?dailies-build=${++buildProbe}`);
|
|
1782
|
+
return res.ok ? null : `${rel} does not build.`;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
273
1785
|
// ---- the agent lane ----------------------------------------------------------
|
|
274
1786
|
|
|
275
1787
|
/**
|
|
@@ -287,3 +1799,67 @@ const agentTransport: AgentTransport = {
|
|
|
287
1799
|
};
|
|
288
1800
|
|
|
289
1801
|
export const agent: AgentClient = createAgentClient(agentTransport);
|
|
1802
|
+
|
|
1803
|
+
// ---- Community component listings (worker componentListings.ts) ----------
|
|
1804
|
+
// The three Studio-side calls on this host. Listing a component, its link and
|
|
1805
|
+
// its preview are written on the main host (`/c/share`), not here.
|
|
1806
|
+
|
|
1807
|
+
const COMPONENT_LISTINGS = "/studio/component-listings";
|
|
1808
|
+
|
|
1809
|
+
/** The owner side: this workspace's listing of a component, if any, with how
|
|
1810
|
+
* many people added each version. */
|
|
1811
|
+
export interface OwnedComponentListing {
|
|
1812
|
+
id: string;
|
|
1813
|
+
status: "draft" | "unlisted" | "published";
|
|
1814
|
+
version: number | null;
|
|
1815
|
+
name: string;
|
|
1816
|
+
people: { version: number; people: number }[];
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
export async function ownedComponentListing(componentId: string): Promise<OwnedComponentListing | null> {
|
|
1820
|
+
return (
|
|
1821
|
+
await request<{ listing: OwnedComponentListing | null }>(
|
|
1822
|
+
`${COMPONENT_LISTINGS}/owned?component=${encodeURIComponent(componentId)}`,
|
|
1823
|
+
)
|
|
1824
|
+
).listing;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/** The copy side: the latest version of each listing these copies came from
|
|
1828
|
+
* (`origin.source`). A copy whose `origin.version` is lower has an update. */
|
|
1829
|
+
export interface ComponentListingUpdate {
|
|
1830
|
+
source: string;
|
|
1831
|
+
version: number;
|
|
1832
|
+
contentHash: string;
|
|
1833
|
+
name: string;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
export async function componentListingUpdates(sources: readonly string[]): Promise<ComponentListingUpdate[]> {
|
|
1837
|
+
const ids = [...new Set(sources)].filter((s) => s.startsWith("cml-")).slice(0, 50);
|
|
1838
|
+
if (ids.length === 0) return [];
|
|
1839
|
+
return (
|
|
1840
|
+
await request<{ updates: ComponentListingUpdate[] }>(`${COMPONENT_LISTINGS}/updates?source=${ids.join(",")}`)
|
|
1841
|
+
).updates;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/** The latest version of a listing this workspace may see, as its frozen
|
|
1845
|
+
* closure, so an update can name what it keeps aside before the yes. */
|
|
1846
|
+
export function componentListingDefinition(
|
|
1847
|
+
listingId: string,
|
|
1848
|
+
): Promise<{ version: number; contentHash: string; closure: DefClosure }> {
|
|
1849
|
+
return request(`${COMPONENT_LISTINGS}/${encodeURIComponent(listingId)}/definition`);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/** Replace the copy `def` in `project` with the listing's latest version.
|
|
1853
|
+
* Overrides move by key (`carryOverrides`); `quarantined` counts the ones
|
|
1854
|
+
* kept under their old names. The store writes the document, so the open
|
|
1855
|
+
* editor receives it as a remote change. */
|
|
1856
|
+
export function applyComponentListingUpdate(
|
|
1857
|
+
listingId: string,
|
|
1858
|
+
project: string,
|
|
1859
|
+
def: string,
|
|
1860
|
+
): Promise<{ defId: string; changed: boolean; quarantined: number; rev: string; version: number }> {
|
|
1861
|
+
return request(`${COMPONENT_LISTINGS}/${encodeURIComponent(listingId)}/update`, {
|
|
1862
|
+
method: "POST",
|
|
1863
|
+
body: JSON.stringify({ project, def, clientId: CLIENT_ID }),
|
|
1864
|
+
});
|
|
1865
|
+
}
|