auricui-skill 0.0.0-stage → 0.1.2

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.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/bin/install.mjs +66 -0
  4. package/package.json +29 -4
  5. package/skill/SKILL.md +64 -0
  6. package/skill/agents/openai.yaml +4 -0
  7. package/skill/assets/examples/ApprovalCard.tsx +8 -0
  8. package/skill/assets/examples/BasicApp.tsx +38 -0
  9. package/skill/assets/examples/BriefingCard.tsx +8 -0
  10. package/skill/assets/examples/ChatControls.tsx +49 -0
  11. package/skill/assets/examples/ChatPanel.tsx +73 -0
  12. package/skill/assets/examples/Checkbox.tsx +13 -0
  13. package/skill/assets/examples/Combobox.tsx +14 -0
  14. package/skill/assets/examples/Drawer.tsx +10 -0
  15. package/skill/assets/examples/MemoryCard.tsx +9 -0
  16. package/skill/assets/examples/MessageList.tsx +8 -0
  17. package/skill/assets/examples/Theme.tsx +14 -0
  18. package/skill/assets/examples/ToolCall.tsx +8 -0
  19. package/skill/assets/examples/Tree.tsx +15 -0
  20. package/skill/assets/examples/examples.css +11 -0
  21. package/skill/assets/library/auricui-0.1.0-preview.1.tgz +0 -0
  22. package/skill/assets/library/auricui-0.1.0-preview.3.tgz +0 -0
  23. package/skill/assets/library/auricui-0.1.0-preview.4.tgz +0 -0
  24. package/skill/assets/upstream/LICENSE +21 -0
  25. package/skill/assets/upstream/apps/desktop/chat/src/App.tsx +1099 -0
  26. package/skill/assets/upstream/apps/desktop/chat/src/app-bridge.ts +154 -0
  27. package/skill/assets/upstream/apps/desktop/chat/src/chat.css +1812 -0
  28. package/skill/assets/upstream/apps/desktop/chat/src/components/AddSkill.tsx +223 -0
  29. package/skill/assets/upstream/apps/desktop/chat/src/components/AgentActivityRow.tsx +716 -0
  30. package/skill/assets/upstream/apps/desktop/chat/src/components/AgentIcons.tsx +46 -0
  31. package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentCard.tsx +34 -0
  32. package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentPreview.tsx +331 -0
  33. package/skill/assets/upstream/apps/desktop/chat/src/components/BalanceNotice.tsx +22 -0
  34. package/skill/assets/upstream/apps/desktop/chat/src/components/BrandLogo.tsx +35 -0
  35. package/skill/assets/upstream/apps/desktop/chat/src/components/ComposerCard.tsx +182 -0
  36. package/skill/assets/upstream/apps/desktop/chat/src/components/ConversationSessionBar.tsx +292 -0
  37. package/skill/assets/upstream/apps/desktop/chat/src/components/CopyDiagnostics.tsx +24 -0
  38. package/skill/assets/upstream/apps/desktop/chat/src/components/Divider.tsx +47 -0
  39. package/skill/assets/upstream/apps/desktop/chat/src/components/FileChangesList.tsx +119 -0
  40. package/skill/assets/upstream/apps/desktop/chat/src/components/Markdown.tsx +198 -0
  41. package/skill/assets/upstream/apps/desktop/chat/src/components/ModelPopover.tsx +280 -0
  42. package/skill/assets/upstream/apps/desktop/chat/src/components/NoticeCard.tsx +87 -0
  43. package/skill/assets/upstream/apps/desktop/chat/src/components/Popover.tsx +195 -0
  44. package/skill/assets/upstream/apps/desktop/chat/src/components/PreviewFrame.tsx +129 -0
  45. package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectConversation.tsx +852 -0
  46. package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectStarters.tsx +118 -0
  47. package/skill/assets/upstream/apps/desktop/chat/src/components/Tooltip.tsx +191 -0
  48. package/skill/assets/upstream/apps/desktop/chat/src/components/TurnLog.tsx +583 -0
  49. package/skill/assets/upstream/apps/desktop/chat/src/components/TurnQuestionCard.tsx +133 -0
  50. package/skill/assets/upstream/apps/desktop/chat/src/components/dock-pane-bar.ts +14 -0
  51. package/skill/assets/upstream/apps/desktop/chat/src/components/project-version.ts +37 -0
  52. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/MentionMenuPlugin.tsx +372 -0
  53. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptEditor.tsx +476 -0
  54. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptPill.tsx +399 -0
  55. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptReferenceNode.tsx +164 -0
  56. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/index.ts +31 -0
  57. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/mention-menu.ts +206 -0
  58. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/prompt-text.ts +171 -0
  59. package/skill/assets/upstream/apps/desktop/chat/src/components/useStickToBottom.ts +320 -0
  60. package/skill/assets/upstream/apps/desktop/chat/src/components/useTicker.ts +55 -0
  61. package/skill/assets/upstream/apps/desktop/chat/src/i18n/index.ts +42 -0
  62. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/en-US.ts +408 -0
  63. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/es.ts +407 -0
  64. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ja.ts +407 -0
  65. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ko.ts +403 -0
  66. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/words.ts +6 -0
  67. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/zh-CN.ts +407 -0
  68. package/skill/assets/upstream/apps/desktop/chat/src/i18n/lookup.ts +12 -0
  69. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-activity-runs.ts +154 -0
  70. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-menu-rows.ts +41 -0
  71. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-refs.ts +105 -0
  72. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-transcript.ts +440 -0
  73. package/skill/assets/upstream/apps/desktop/chat/src/lib/asset-lists.ts +75 -0
  74. package/skill/assets/upstream/apps/desktop/chat/src/lib/attachment-place.ts +17 -0
  75. package/skill/assets/upstream/apps/desktop/chat/src/lib/balance.ts +16 -0
  76. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-layer.ts +10 -0
  77. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-menu.ts +18 -0
  78. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-store.ts +101 -0
  79. package/skill/assets/upstream/apps/desktop/chat/src/lib/composer-attachments.ts +189 -0
  80. package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-attachments.ts +178 -0
  81. package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-bridge.ts +291 -0
  82. package/skill/assets/upstream/apps/desktop/chat/src/lib/developer.ts +39 -0
  83. package/skill/assets/upstream/apps/desktop/chat/src/lib/esc-stop.ts +23 -0
  84. package/skill/assets/upstream/apps/desktop/chat/src/lib/file-changes.ts +32 -0
  85. package/skill/assets/upstream/apps/desktop/chat/src/lib/live-activity.ts +20 -0
  86. package/skill/assets/upstream/apps/desktop/chat/src/lib/local-agent-run.ts +1174 -0
  87. package/skill/assets/upstream/apps/desktop/chat/src/lib/local-sessions.ts +25 -0
  88. package/skill/assets/upstream/apps/desktop/chat/src/lib/model-calls.ts +87 -0
  89. package/skill/assets/upstream/apps/desktop/chat/src/lib/pretty-json.ts +12 -0
  90. package/skill/assets/upstream/apps/desktop/chat/src/lib/preview-top.ts +15 -0
  91. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-resources.ts +66 -0
  92. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-sessions.ts +66 -0
  93. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-versions.ts +116 -0
  94. package/skill/assets/upstream/apps/desktop/chat/src/lib/ref-pictures.ts +119 -0
  95. package/skill/assets/upstream/apps/desktop/chat/src/lib/relative-time.ts +46 -0
  96. package/skill/assets/upstream/apps/desktop/chat/src/lib/skill-pages.ts +29 -0
  97. package/skill/assets/upstream/apps/desktop/chat/src/lib/step-file.ts +23 -0
  98. package/skill/assets/upstream/apps/desktop/chat/src/lib/structural-share.ts +52 -0
  99. package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-film.ts +44 -0
  100. package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-transfer.ts +40 -0
  101. package/skill/assets/upstream/apps/desktop/chat/src/lib/unprompted.ts +27 -0
  102. package/skill/assets/upstream/apps/desktop/chat/src/lib/upload-limits.ts +53 -0
  103. package/skill/assets/upstream/apps/desktop/chat/src/mount.tsx +19 -0
  104. package/skill/assets/upstream/apps/desktop/chat/src/state/local-agent-store.ts +296 -0
  105. package/skill/assets/upstream/apps/desktop/chat/src/studio-refs.ts +307 -0
  106. package/skill/assets/upstream/apps/desktop/package.json +51 -0
  107. package/skill/assets/upstream/apps/desktop/shell/bridge.ts +415 -0
  108. package/skill/assets/upstream/apps/desktop/shell/index.html +27 -0
  109. package/skill/assets/upstream/apps/desktop/shell/main.tsx +17 -0
  110. package/skill/assets/upstream/apps/desktop/shell/shell.css +17 -0
  111. package/skill/assets/upstream/apps/desktop/shell/vite.config.ts +65 -0
  112. package/skill/assets/upstream/apps/desktop/src/agent-definitions.mjs +31 -0
  113. package/skill/assets/upstream/packages/openfilm/LICENSE +21 -0
  114. package/skill/assets/upstream/packages/openfilm/package.json +88 -0
  115. package/skill/assets/upstream/packages/openfilm/src/film-doc.d.mts +125 -0
  116. package/skill/assets/upstream/packages/openfilm/src/film-doc.mjs +986 -0
  117. package/skill/assets/upstream/packages/openfilm/src/overrides.mjs +73 -0
  118. package/skill/assets/upstream/packages/openfilm/src/timeline.mjs +1280 -0
  119. package/skill/assets/upstream/packages/openfilm/studio/server/bridge.js +209 -0
  120. package/skill/assets/upstream/packages/openfilm/studio/server/film-subtitle.mjs +785 -0
  121. package/skill/assets/upstream/packages/openfilm/studio/server/ops.mjs +403 -0
  122. package/skill/assets/upstream/packages/openfilm/studio/server/stage.js +1649 -0
  123. package/skill/assets/upstream/packages/openfilm/studio/server/track-layout.mjs +43 -0
  124. package/skill/assets/upstream/packages/openfilm/studio/ui/src/App.tsx +156 -0
  125. package/skill/assets/upstream/packages/openfilm/studio/ui/src/api.ts +328 -0
  126. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/AudioMeters.tsx +188 -0
  127. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockThumbs.tsx +84 -0
  128. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockWaveform.tsx +201 -0
  129. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLoadingScreen.tsx +56 -0
  130. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLogo.tsx +25 -0
  131. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Card.tsx +89 -0
  132. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ColorPicker.tsx +204 -0
  133. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ConfirmDialog.tsx +96 -0
  134. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ContextMenu.tsx +422 -0
  135. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/DesktopDialog.tsx +271 -0
  136. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ExamplesGallery.tsx +372 -0
  137. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmFullscreenBar.tsx +147 -0
  138. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmInspector.tsx +2310 -0
  139. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmStageSelect.tsx +3296 -0
  140. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FocusNav.tsx +28 -0
  141. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FontFamilyPicker.tsx +293 -0
  142. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FramePicker.tsx +69 -0
  143. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/InspectorPanel.tsx +25 -0
  144. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Launcher.tsx +38 -0
  145. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LayersPanel.tsx +277 -0
  146. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LiveTime.tsx +101 -0
  147. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPool.tsx +1413 -0
  148. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPreview.tsx +188 -0
  149. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MenuSelect.tsx +161 -0
  150. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MgOutline.tsx +24 -0
  151. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/OperationHistory.tsx +184 -0
  152. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PaneDivider.tsx +292 -0
  153. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PasteAttrsDialog.tsx +69 -0
  154. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PlaybackMenu.tsx +110 -0
  155. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Popover.tsx +159 -0
  156. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectCover.tsx +60 -0
  157. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectHistoryControls.tsx +748 -0
  158. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectLayoutMenu.tsx +82 -0
  159. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectMenu.tsx +111 -0
  160. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectSources.tsx +587 -0
  161. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectView.tsx +3222 -0
  162. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsLibrary.tsx +567 -0
  163. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsView.tsx +1593 -0
  164. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PropertiesSection.tsx +418 -0
  165. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ResourceInspector.tsx +100 -0
  166. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SelectionToolbar.tsx +157 -0
  167. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SkillsLibrary.tsx +504 -0
  168. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SourcePreview.tsx +464 -0
  169. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SpendAsks.tsx +88 -0
  170. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageMaskOverlay.tsx +299 -0
  171. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageTransformFrame.tsx +246 -0
  172. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.module.css +46 -0
  173. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.tsx +884 -0
  174. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleOverlay.tsx +148 -0
  175. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleRow.tsx +419 -0
  176. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TabStrip.tsx +188 -0
  177. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Timeline.tsx +4911 -0
  178. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TimelineMarkers.tsx +329 -0
  179. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Toast.tsx +55 -0
  180. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Tooltip.tsx +193 -0
  181. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/UpdateButton.tsx +104 -0
  182. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ViewerBar.tsx +98 -0
  183. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/WhatsNew.tsx +147 -0
  184. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/dock-pane-bar.ts +16 -0
  185. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportCenter.tsx +1502 -0
  186. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportTray.tsx +284 -0
  187. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/film-stage-hit.ts +18 -0
  188. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-fields.tsx +863 -0
  189. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-look.tsx +1357 -0
  190. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/controls-bar.tsx +397 -0
  191. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/transport.tsx +294 -0
  192. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/replace-drop.ts +20 -0
  193. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/AgentsSection.tsx +613 -0
  194. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/CustomServices.tsx +104 -0
  195. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/DiagnosticsGroup.tsx +202 -0
  196. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ModelsSection.tsx +179 -0
  197. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProviderMark.tsx +24 -0
  198. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProvidersSection.tsx +177 -0
  199. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsModal.tsx +511 -0
  200. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsSelect.tsx +84 -0
  201. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ShortcutsSection.tsx +291 -0
  202. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/StorageGroup.tsx +119 -0
  203. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/theme.ts +47 -0
  204. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ui.tsx +139 -0
  205. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/use-settings.ts +10 -0
  206. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/stage-menu.tsx +283 -0
  207. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/timeline-head-extras.tsx +91 -0
  208. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/typing-target.ts +45 -0
  209. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-editor-tools.tsx +408 -0
  210. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-page-changes.ts +48 -0
  211. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-project-history.ts +113 -0
  212. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/useBlockDrag.ts +639 -0
  213. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/Preview.tsx +426 -0
  214. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/player.ts +558 -0
  215. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/preview-frames.ts +36 -0
  216. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/steps.ts +36 -0
  217. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/undo.ts +156 -0
  218. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-film.ts +212 -0
  219. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-preview-renders.ts +89 -0
  220. package/skill/assets/upstream/packages/openfilm/studio/ui/src/globals.css +559 -0
  221. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/en.ts +1901 -0
  222. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/es.ts +1900 -0
  223. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/index.ts +130 -0
  224. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ja.ts +1900 -0
  225. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ko.ts +1896 -0
  226. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/words.ts +6 -0
  227. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/zh-CN.ts +1901 -0
  228. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/agent-row.ts +21 -0
  229. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/asset-timeline.ts +469 -0
  230. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/audio-levels.ts +60 -0
  231. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/batch-map.ts +28 -0
  232. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/browser.ts +10 -0
  233. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/canvas-fit.ts +116 -0
  234. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/chat-marks.ts +114 -0
  235. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-attrs.ts +102 -0
  236. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-fade.ts +70 -0
  237. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-look.ts +520 -0
  238. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-mask.ts +536 -0
  239. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-replace.ts +60 -0
  240. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/component-props.ts +128 -0
  241. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/context-length.ts +12 -0
  242. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/desktop-build.ts +44 -0
  243. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/diagnostics.ts +68 -0
  244. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/double-press.ts +22 -0
  245. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/dropped-overrides.ts +139 -0
  246. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-jobs.ts +191 -0
  247. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-spec.ts +261 -0
  248. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/field-input.ts +155 -0
  249. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-clip-edits.ts +82 -0
  250. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-frame.ts +134 -0
  251. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-overrides.ts +107 -0
  252. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-shape.ts +172 -0
  253. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film.ts +80 -0
  254. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-menu.ts +236 -0
  255. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-styles.ts +129 -0
  256. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/freeze-frame.ts +49 -0
  257. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/host.ts +438 -0
  258. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/inspect-scrub.ts +26 -0
  259. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap-store.ts +74 -0
  260. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap.ts +529 -0
  261. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/layers-tree.ts +133 -0
  262. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/look.ts +83 -0
  263. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/loudness.ts +58 -0
  264. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/markers.ts +152 -0
  265. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-bar.ts +58 -0
  266. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-folders.ts +45 -0
  267. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-place.ts +102 -0
  268. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-queue.ts +107 -0
  269. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-selection.ts +64 -0
  270. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/menu-position.ts +77 -0
  271. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mg-transform.ts +111 -0
  272. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mixed.ts +3 -0
  273. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/page-changes.ts +45 -0
  274. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/picture-files.ts +19 -0
  275. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-prefs.ts +46 -0
  276. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-renders.ts +71 -0
  277. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-delete.ts +20 -0
  278. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-folders.ts +144 -0
  279. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-history.ts +188 -0
  280. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-sources.ts +283 -0
  281. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/projects-page.ts +40 -0
  282. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/provider-marks.ts +35 -0
  283. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/providers.ts +163 -0
  284. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/resource-drag.ts +131 -0
  285. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/retry-image.ts +17 -0
  286. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved-insert.ts +40 -0
  287. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved.ts +125 -0
  288. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/selection-toolbar.ts +158 -0
  289. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/shortcut-hint.ts +21 -0
  290. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/similar-layers.ts +46 -0
  291. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/solo.ts +19 -0
  292. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-clip.ts +60 -0
  293. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-crop.ts +232 -0
  294. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-gesture.ts +277 -0
  295. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-layers.ts +71 -0
  296. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-mask.ts +176 -0
  297. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-measure.ts +53 -0
  298. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-types.ts +155 -0
  299. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-refs.ts +248 -0
  300. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-version.ts +41 -0
  301. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitle-cues.ts +238 -0
  302. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitles.ts +263 -0
  303. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/tab-menu.ts +6 -0
  304. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/template-controls.ts +131 -0
  305. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/theme-changes.ts +31 -0
  306. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cache.ts +63 -0
  307. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cell.ts +8 -0
  308. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-frame.ts +76 -0
  309. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-pick.ts +42 -0
  310. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timecode.ts +161 -0
  311. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-clipboard.ts +279 -0
  312. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-drag.ts +589 -0
  313. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-edit.ts +1138 -0
  314. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-keys.ts +63 -0
  315. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-layout.ts +1038 -0
  316. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-nav.ts +160 -0
  317. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-prefs.ts +73 -0
  318. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-split.ts +40 -0
  319. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-view.ts +105 -0
  320. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/title.ts +39 -0
  321. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-heights.ts +55 -0
  322. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-lock.ts +27 -0
  323. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/transitions.ts +459 -0
  324. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/type-metrics.ts +83 -0
  325. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-film-subtitles.ts +111 -0
  326. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-fonts.ts +181 -0
  327. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-loaded-thumbs.ts +91 -0
  328. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-edits.ts +134 -0
  329. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-font.ts +51 -0
  330. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-style.ts +68 -0
  331. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/waveform-columns.ts +48 -0
  332. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/web-update.ts +41 -0
  333. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/whats-new.ts +401 -0
  334. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/workspace-resources.ts +373 -0
  335. package/skill/assets/upstream/packages/openfilm/studio/ui/src/main.tsx +10 -0
  336. package/skill/assets/upstream/packages/openfilm/studio/ui/vite.config.ts +12 -0
  337. package/skill/assets/upstream/packages/shared/package.json +45 -0
  338. package/skill/assets/upstream/packages/shared/src/agent-activity.ts +263 -0
  339. package/skill/assets/upstream/packages/shared/src/contract.ts +285 -0
  340. package/skill/assets/upstream/packages/shared/src/index.ts +17 -0
  341. package/skill/assets/upstream/packages/shared/src/markdown-blocks.ts +252 -0
  342. package/skill/assets/upstream/packages/shared/src/modality.ts +4 -0
  343. package/skill/assets/upstream/packages/shared/src/prompt-reference.ts +113 -0
  344. package/skill/assets/upstream/packages/shared/src/title.ts +76 -0
  345. package/skill/assets/upstream/packages/shared/src/turn.ts +68 -0
  346. package/skill/assets/upstream/packages/shared/src/video.ts +26 -0
  347. package/skill/references/agent-components.md +33 -0
  348. package/skill/references/api-index.json +2693 -0
  349. package/skill/references/chat-components.md +65 -0
  350. package/skill/references/components.md +23 -0
  351. package/skill/references/core-components.md +44 -0
  352. package/skill/references/dependencies.md +22 -0
  353. package/skill/references/esm-package.md +48 -0
  354. package/skill/references/integration.md +43 -0
  355. package/skill/references/library-manifest.json +12 -0
  356. package/skill/references/source-manifest.json +325 -0
  357. package/skill/references/studio-components.md +24 -0
  358. package/skill/references/theme-motion.md +49 -0
  359. package/skill/references/troubleshooting.md +27 -0
  360. package/skill/references/verification.md +13 -0
  361. package/skill/scripts/check_dependencies.py +109 -0
  362. package/skill/scripts/inspect_api.py +60 -0
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Transitions, made of what film.html already says: fades (each clip's own `fade`, see lib/clip-fade) and where clips
3
+ * are. Nothing new is written in the file, so an agent reads a transition as two clips that fade.
4
+ *
5
+ * Cross dissolve the incoming clip (with the clips linked to it) moves d earlier so the two overlap by d, onto a
6
+ * track with room: a picture above the outgoing clip's track (the one right above when it is free
7
+ * there, else a new one just above), a sound below. The incoming picture fades in over d while the
8
+ * outgoing one stays whole under it, so the two blend (both fading would dip through the black
9
+ * under them); sounds both fade, out and in. With `ripple`, what follows the cut moves d
10
+ * earlier too, on every unlocked track, so no gap opens and nothing comes apart; without it, what
11
+ * follows stays and a gap of d is left after the incoming clip.
12
+ * Audio crossfade the same, for two sounds (a sound track below).
13
+ * Dip to black no clip moves: the outgoing clip fades out over d/2, the incoming one in over d/2. The picture dips
14
+ * to what is under them: the stage's black where nothing is.
15
+ * Dip to white the same, over a white page (`transitions/white.html`) cut in under the dip, on the track below.
16
+ * Fade in / out one clip's fade.
17
+ * Dropped on a clip's edge with no clip against it, a cross dissolve or a dip is that clip's fade (a dip to white
18
+ * with its white page under it).
19
+ *
20
+ * The clips linked to a clip go with it (Linked switch; ⌥ for one): their fades at the same edge too. Nothing on a
21
+ * locked track changes: a transition that would change one is refused.
22
+ *
23
+ * On the timeline a transition is found again from the fades (findTransitions): two clips whose fades meet as one of
24
+ * the above made them. Removed, its fades go and its clips come back as they were cut (the incoming clip back against
25
+ * the outgoing one, on its track when there is room, what follows moving back with `ripple`); resized, the clips
26
+ * move and the fades change by the difference.
27
+ *
28
+ * Pure: every plan is film.html's operations, one write and one undo step.
29
+ */
30
+ import type { Op, PropValue } from '../api.ts';
31
+ import {
32
+ TOUCH_MS, draftOf, findInDraft, follow, linkedIds, planOf, rippleAt, trackAt,
33
+ type Draft, type EditClip, type EditModel, type EditTrack, type TrackDest,
34
+ } from './timeline-edit.ts';
35
+
36
+ export type TransitionKind = 'dissolve' | 'crossfade' | 'dip-black' | 'dip-white' | 'fade-in' | 'fade-out';
37
+ /** The kinds that are found again on the timeline (a fade alone is a fade). */
38
+ export type FoundKind = 'dissolve' | 'crossfade' | 'dip-black' | 'dip-white';
39
+
40
+ /** The default length of a transition (ms). */
41
+ export const TRANSITION_MS = 500;
42
+
43
+ /** What a plan needs to know of each clip that the edit model does not say. */
44
+ export interface ClipFacts {
45
+ /** It has a picture (a page, a video, a still); else a sound. */
46
+ picture: boolean;
47
+ /** Its fades, ms. */
48
+ fade: readonly [number, number];
49
+ /** It is the white page under a dip to white. */
50
+ white?: boolean;
51
+ }
52
+ export type FactsOf = (id: string) => ClipFacts | undefined;
53
+
54
+ /** A cut: the clip ending there (`a`) and the one starting there (`b`); one of them alone is a clip's edge. */
55
+ export interface CutRef { a: string | null; b: string | null }
56
+
57
+ export interface FilmTransition {
58
+ /** Its name on the timeline (stays while its clips do). */
59
+ key: string;
60
+ kind: FoundKind;
61
+ a: string;
62
+ b: string;
63
+ startMs: number;
64
+ endMs: number;
65
+ /** The clip whose row it is drawn on: the incoming one's for an overlap, the outgoing one's for a dip. */
66
+ on: string;
67
+ /** The white page under a dip to white. */
68
+ white?: string;
69
+ /** Where the cut is: the outgoing clip's end. */
70
+ cutMs: number;
71
+ }
72
+
73
+ /** What the timeline asks of the editor: a transition put on a cut, made longer or shorter, or taken away. */
74
+ export type TransitionAsk =
75
+ | { kind: 'add'; type: TransitionKind; cut: CutRef }
76
+ | { kind: 'resize'; key: string; durationMs: number }
77
+ | { kind: 'remove'; key: string };
78
+
79
+ /** Where a transition is drawn when it is `durationMs` long: a dip around its cut, an overlap ending where it ends. */
80
+ export function transitionSpan(tr: FilmTransition, durationMs: number): { startMs: number; endMs: number } {
81
+ if (tr.kind === 'dip-black' || tr.kind === 'dip-white') return { startMs: tr.cutMs - durationMs / 2, endMs: tr.cutMs + durationMs / 2 };
82
+ return { startMs: tr.endMs - durationMs, endMs: tr.endMs };
83
+ }
84
+
85
+ export interface TransitionOpts {
86
+ /** An overlap closes up what follows on its tracks (the main track's magnet), removing one opens it again. */
87
+ ripple: boolean;
88
+ /** Linked clips go with each other (the Linked switch, unless ⌥). */
89
+ linked: boolean;
90
+ /** The white page's path (dip to white). */
91
+ whiteSrc?: string;
92
+ }
93
+
94
+ export type TransitionPlan = { ops: Op[]; shortMs: number } | { error: 'locked' | 'none' | 'short' };
95
+
96
+ /* ── finding them again ── */
97
+
98
+ const EPS = 1.5; // ms: times are kept to the ms, each rounded alone
99
+
100
+ const allClips = (model: EditModel): { clip: EditClip; track: EditTrack }[] => model.flatMap((track) => track.clips.map((clip) => ({ clip, track })));
101
+
102
+ /** The transitions the fades on the timeline make (see the top). */
103
+ export function findTransitions(model: EditModel, facts: FactsOf): FilmTransition[] {
104
+ const clips = allClips(model);
105
+ const used = new Set<string>();
106
+ const out: FilmTransition[] = [];
107
+ const whites = clips.filter(({ clip }) => facts(clip.id)?.white);
108
+ for (const { clip: a, track: ta } of [...clips].sort((x, y) => Number(facts(y.clip.id)?.picture) - Number(facts(x.clip.id)?.picture))) {
109
+ const fa = facts(a.id);
110
+ if (!fa || fa.white || used.has(`a:${a.id}`)) continue;
111
+ for (const { clip: b, track: tb } of clips) {
112
+ const fb = facts(b.id);
113
+ if (!fb || b.id === a.id || fb.white || fb.picture !== fa.picture || !(fb.fade[0] > 0) || used.has(`b:${b.id}`)) continue;
114
+ /* an overlap the incoming clip fades in over: the outgoing one fades out with it, or (a picture under it) stays whole */
115
+ const lap = a.endMs - b.startMs;
116
+ const overlap = lap > EPS && tb.index !== ta.index && Math.abs(fb.fade[0] - lap) <= EPS
117
+ && (Math.abs(fa.fade[1] - lap) <= EPS || (fa.picture && fa.fade[1] <= EPS && tb.index < ta.index));
118
+ const touch = fa.fade[1] > 0 && Math.abs(b.startMs - a.endMs) <= EPS && tb.index === ta.index;
119
+ if (!overlap && !touch) continue;
120
+ let kind: FoundKind = overlap ? (fa.picture ? 'dissolve' : 'crossfade') : 'dip-black';
121
+ const startMs = overlap ? b.startMs : a.endMs - fa.fade[1];
122
+ const endMs = overlap ? a.endMs : b.startMs + fb.fade[0];
123
+ let white: string | undefined;
124
+ if (touch && fa.picture) {
125
+ white = whites.find(({ clip: w }) => Math.abs(w.startMs - startMs) <= EPS && Math.abs(w.endMs - endMs) <= EPS)?.clip.id;
126
+ if (white) kind = 'dip-white';
127
+ }
128
+ used.add(`a:${a.id}`);
129
+ used.add(`b:${b.id}`);
130
+ out.push({ key: `${a.id}|${b.id}`, kind, a: a.id, b: b.id, startMs, endMs, on: overlap ? b.id : a.id, cutMs: a.endMs, ...(white ? { white } : {}) });
131
+ break;
132
+ }
133
+ }
134
+ /* a picture's transition takes the sounds linked to it along: theirs is part of it, not one of its own */
135
+ const linkOf = new Map(clips.map(({ clip }) => [clip.id, clip.link]));
136
+ const pictures = out.filter((x) => facts(x.a)?.picture);
137
+ return out.filter((x) => facts(x.a)?.picture || !pictures.some((p) => linkOf.get(p.a) && linkOf.get(p.a) === linkOf.get(x.a) && linkOf.get(p.b) && linkOf.get(p.b) === linkOf.get(x.b)));
138
+ }
139
+
140
+ /** The cut on track `track` at `ms` (within `tolMs`): the clip ending there and the one starting there, either alone. */
141
+ export function cutNear(model: EditModel, track: number, ms: number, tolMs: number): (CutRef & { atMs: number }) | null {
142
+ const clips = model.find((t) => t.index === track)?.clips ?? [];
143
+ const ends = clips.filter((c) => Math.abs(c.endMs - ms) <= tolMs).sort((x, y) => Math.abs(x.endMs - ms) - Math.abs(y.endMs - ms));
144
+ const starts = clips.filter((c) => Math.abs(c.startMs - ms) <= tolMs).sort((x, y) => Math.abs(x.startMs - ms) - Math.abs(y.startMs - ms));
145
+ const a = ends[0], b = starts[0];
146
+ if (a && b && Math.abs(a.endMs - b.startMs) <= TOUCH_MS) return { a: a.id, b: b.id, atMs: b.startMs };
147
+ if (a && (!b || Math.abs(a.endMs - ms) <= Math.abs(b.startMs - ms))) return { a: a.id, b: null, atMs: a.endMs };
148
+ if (b) return { a: null, b: b.id, atMs: b.startMs };
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Where a tile clicked goes. Among the clips selected: a cut between two of them, else the cut or edge of theirs nearest
154
+ * the playhead; with none selected, the cut nearest the playhead on the targeted track (else on any unlocked one). A
155
+ * fade in goes on a clip's start, a fade out on its end; a transition goes on a cut before a lone edge.
156
+ */
157
+ export function cutForClick(model: EditModel, kind: TransitionKind, selected: readonly string[], playheadMs: number, target: number | null): CutRef | null {
158
+ const open = model.filter((t) => !t.locked);
159
+ const pick = new Set(selected);
160
+ const tracks = pick.size ? open : open.filter((t) => t.index === target && t.clips.length).length ? open.filter((t) => t.index === target) : open;
161
+ type Edge = CutRef & { atMs: number; both: boolean };
162
+ const edges: Edge[] = [];
163
+ for (const t of tracks) {
164
+ for (const c of t.clips) {
165
+ if (pick.size && !pick.has(c.id)) continue;
166
+ const before = t.clips.find((x) => x.id !== c.id && Math.abs(x.endMs - c.startMs) <= TOUCH_MS);
167
+ const after = t.clips.find((x) => x.id !== c.id && Math.abs(x.startMs - c.endMs) <= TOUCH_MS);
168
+ if (kind !== 'fade-out') edges.push({ a: before?.id ?? null, b: c.id, atMs: c.startMs, both: Boolean(before && pick.has(before.id)) });
169
+ if (kind !== 'fade-in') edges.push({ a: c.id, b: after?.id ?? null, atMs: c.endMs, both: Boolean(after && pick.has(after.id)) });
170
+ }
171
+ }
172
+ if (!edges.length) return null;
173
+ const fade = kind === 'fade-in' || kind === 'fade-out';
174
+ const rank = (e: Edge) => (fade ? 0 : e.both ? 0 : e.a && e.b ? 1 : 2);
175
+ const best = [...edges].sort((x, y) => rank(x) - rank(y) || Math.abs(x.atMs - playheadMs) - Math.abs(y.atMs - playheadMs))[0]!;
176
+ /* nothing selected: a lone edge only for a fade (a transition wants a cut) */
177
+ if (!pick.size && !fade && !(best.a && best.b)) return null;
178
+ return { a: best.a, b: best.b };
179
+ }
180
+
181
+ /* ── planning ── */
182
+
183
+ /** The fades an edit sets, by clip id (ms); written after the moves, in the same edit. */
184
+ type Fades = Map<string, [number, number]>;
185
+
186
+ const lengthOf = (c: EditClip) => c.endMs - c.startMs;
187
+ const free = (track: EditTrack, startMs: number, endMs: number, skip: ReadonlySet<string>) => !track.clips.some(
188
+ (c) => !skip.has(c.id) && c.startMs < endMs - TOUCH_MS && c.endMs > startMs + TOUCH_MS,
189
+ );
190
+
191
+ /** A ripple from a transition's edit, on the tracks it changes (timeline-edit follow). */
192
+ function rippleThrough(
193
+ d: Draft,
194
+ point: number,
195
+ deltaMs: number,
196
+ edited: Iterable<number>,
197
+ skip?: ReadonlySet<string>,
198
+ ): void {
199
+ const f = follow(d, edited);
200
+ rippleAt(d, point, deltaMs, f.tracks, skip, f.bound);
201
+ }
202
+
203
+ function fadeOf(fades: Fades, facts: FactsOf, id: string): [number, number] {
204
+ const had = fades.get(id) ?? facts(id)?.fade ?? [0, 0];
205
+ return [had[0], had[1]];
206
+ }
207
+
208
+ /** Set one edge's fade, fitted to the clip's length with the other fade (which gives way when it has to). */
209
+ function setFade(fades: Fades, facts: FactsOf, clip: EditClip, edge: 'in' | 'out', ms: number) {
210
+ const [fin, fout] = fadeOf(fades, facts, clip.id);
211
+ const len = lengthOf(clip);
212
+ const own = Math.max(0, Math.min(ms, len));
213
+ fades.set(clip.id, edge === 'in' ? [own, Math.min(fout, len - own)] : [Math.min(fin, len - own), own]);
214
+ }
215
+
216
+ /** The clips linked to `id` (it among them) when linked clips go together. */
217
+ function groupOf(model: EditModel, id: string, opts: TransitionOpts): string[] {
218
+ return opts.linked ? linkedIds(model, [id]) : [id];
219
+ }
220
+
221
+ /** `id` and the clips going with it whose `edge` is where its own is. */
222
+ function atEdge(d: Draft, model: EditModel, id: string, edge: 'in' | 'out', opts: TransitionOpts): EditClip[] {
223
+ const self = findInDraft(d, id)?.clip;
224
+ if (!self) return [];
225
+ const at = (c: EditClip) => (edge === 'in' ? c.startMs : c.endMs);
226
+ return groupOf(model, id, opts).map((x) => findInDraft(d, x)?.clip).filter((c): c is EditClip => Boolean(c && Math.abs(at(c) - at(self)) <= EPS));
227
+ }
228
+
229
+ const lockedIn = (d: Draft, ids: Iterable<string>) => [...ids].some((id) => findInDraft(d, id)?.track.locked);
230
+
231
+ /** The plan's operations: the moves (planOf), then the fades, then what it adds. */
232
+ function opsOf(d: Draft, fades: Fades, extra: Op[] = []): { ops: Op[]; shortMs: number } {
233
+ const plan = planOf(d);
234
+ const edits = [...fades].map(([clip, [fin, fout]]) => ({ clip, prop: 'fade', value: [secs(fin), secs(fout)] as PropValue }));
235
+ return { ops: [...plan.ops, ...(edits.length ? [{ op: 'props' as const, edits }] : []), ...extra], shortMs: plan.shortMs };
236
+ }
237
+ const secs = (ms: number) => Math.round(ms) / 1000;
238
+
239
+ /** Synthetic tracks for clips going to a new one: their index is out of film.html's range (planOf writes the insert). */
240
+ let openedSeq = 0;
241
+ function openTrack(d: Draft, clip: EditClip, from: EditTrack, insertAt: number) {
242
+ from.clips = from.clips.filter((c) => c.id !== clip.id);
243
+ d.tracks.push({ index: 1_000_000 + openedSeq++, clips: [clip] });
244
+ d.opened.set(clip.id, insertAt);
245
+ }
246
+ function moveTo(clip: EditClip, from: EditTrack, to: EditTrack) {
247
+ if (from === to) return;
248
+ from.clips = from.clips.filter((c) => c.id !== clip.id);
249
+ to.clips.push(clip);
250
+ to.clips.sort((x, y) => x.startMs - y.startMs);
251
+ }
252
+
253
+ /**
254
+ * Where a clip moved to `[startMs, endMs)` goes: its own track when it has room there, else the track right beside
255
+ * `against` (above for a picture, below for a sound) when that is free and of its kind, else a new one there.
256
+ */
257
+ function landing(d: Draft, clip: EditClip, picture: boolean, against: number, skip: ReadonlySet<string>): void {
258
+ const here = findInDraft(d, clip.id)!;
259
+ if (free(here.track, clip.startMs, clip.endMs, new Set([...skip, clip.id]))) return;
260
+ const side = trackAt(d, picture ? against - 1 : against + 1);
261
+ if (side && !side.locked && (picture ? side.role !== 'audio' : side.role === 'audio') && free(side, clip.startMs, clip.endMs, skip)) {
262
+ moveTo(clip, here.track, side);
263
+ return;
264
+ }
265
+ openTrack(d, clip, here.track, picture ? against : against + 1);
266
+ }
267
+
268
+ /** The clip on another track that `x` is cut against: one ending where `x` started (+ `shiftMs`), of its kind. */
269
+ function partnerTrack(d: Draft, facts: FactsOf, x: EditClip, at: number, skip: ReadonlySet<string>): number | null {
270
+ const picture = facts(x.id)?.picture;
271
+ for (const t of d.tracks) {
272
+ for (const c of t.clips) {
273
+ if (skip.has(c.id) || c.id === x.id || facts(c.id)?.picture !== picture) continue;
274
+ if (Math.abs(c.endMs - at) <= EPS) return t.index;
275
+ }
276
+ }
277
+ return null;
278
+ }
279
+
280
+ /** Add a transition at a cut. */
281
+ export function planTransition(model: EditModel, facts: FactsOf, kind: TransitionKind, cut: CutRef, durationMs: number, opts: TransitionOpts): TransitionPlan {
282
+ const d = draftOf(model);
283
+ const fades: Fades = new Map();
284
+ const a = cut.a ? findInDraft(d, cut.a) : null;
285
+ const b = cut.b ? findInDraft(d, cut.b) : null;
286
+ if (!a && !b) return { error: 'none' };
287
+ const ms = Math.max(1, Math.round(durationMs));
288
+
289
+ /* one clip's fade: a fade preset, or a transition on an edge with no clip against it */
290
+ const fadeEdge = (one: { clip: EditClip }, edge: 'in' | 'out', len: number): TransitionPlan => {
291
+ const touched = atEdge(d, model, one.clip.id, edge, opts);
292
+ if (lockedIn(d, touched.map((c) => c.id))) return { error: 'locked' };
293
+ for (const c of touched) setFade(fades, facts, c, edge, len);
294
+ if (kind !== 'dip-white' || !opts.whiteSrc || !facts(one.clip.id)?.picture) return opsOf(d, fades);
295
+ const len2 = Math.min(len, lengthOf(one.clip));
296
+ const span = edge === 'in' ? { startMs: one.clip.startMs, endMs: one.clip.startMs + len2 } : { startMs: one.clip.endMs - len2, endMs: one.clip.endMs };
297
+ return opsOf(d, fades, [whiteInsert(d, opts.whiteSrc, span, findInDraft(d, one.clip.id)!.track.index)]);
298
+ };
299
+ if (kind === 'fade-in') return fadeEdge((b ?? a)!, 'in', ms);
300
+ if (kind === 'fade-out') return fadeEdge((a ?? b)!, 'out', ms);
301
+ if (!a) return fadeEdge(b!, 'in', ms);
302
+ if (!b) return fadeEdge(a, 'out', ms);
303
+
304
+ if (kind === 'dip-black' || kind === 'dip-white') {
305
+ /* half each side of the cut, as far as each clip has room */
306
+ const half = Math.min(ms / 2, lengthOf(a.clip), lengthOf(b.clip));
307
+ const touched = [...atEdge(d, model, a.clip.id, 'out', opts), ...atEdge(d, model, b.clip.id, 'in', opts)];
308
+ if (lockedIn(d, touched.map((c) => c.id))) return { error: 'locked' };
309
+ for (const c of atEdge(d, model, a.clip.id, 'out', opts)) setFade(fades, facts, c, 'out', half);
310
+ for (const c of atEdge(d, model, b.clip.id, 'in', opts)) setFade(fades, facts, c, 'in', half);
311
+ if (kind === 'dip-black' || !opts.whiteSrc) return opsOf(d, fades);
312
+ const lowest = Math.max(a.track.index, b.track.index);
313
+ return opsOf(d, fades, [whiteInsert(d, opts.whiteSrc, { startMs: a.clip.endMs - half, endMs: b.clip.startMs + half }, lowest)]);
314
+ }
315
+
316
+ /* an overlap: the incoming clip and those linked to it move d earlier */
317
+ const overlap = Math.min(ms, lengthOf(a.clip), lengthOf(b.clip));
318
+ if (overlap < 1) return { error: 'short' };
319
+ const moving = groupOf(model, b.clip.id, opts).map((id) => findInDraft(d, id)).filter((x): x is { track: EditTrack; clip: EditClip } => Boolean(x));
320
+ const outgoing = atEdge(d, model, a.clip.id, 'out', opts);
321
+ if (lockedIn(d, [...moving.map((m) => m.clip.id), ...outgoing.map((c) => c.id)])) return { error: 'locked' };
322
+ const skip = new Set(moving.map((m) => m.clip.id));
323
+ const cutMs = a.clip.endMs;
324
+ /* each one's track before it moves: what follows the cut on it closes up after it */
325
+ const homes = new Set([a.track.index, ...moving.map((m) => m.track.index)]);
326
+ const incoming = atEdge(d, model, b.clip.id, 'in', opts);
327
+ /* the clip each is cut against (its track is where it overlaps from), then taken off while what follows closes up,
328
+ then put down d earlier where there is room (see landing) */
329
+ const against = new Map(moving.map((m) => [m.clip.id, partnerTrack(d, facts, m.clip, m.clip.startMs, skip) ?? m.track.index]));
330
+ for (const m of moving) m.track.clips = m.track.clips.filter((c) => c.id !== m.clip.id);
331
+ if (opts.ripple) rippleThrough(d, cutMs, -overlap, homes);
332
+ for (const m of moving) {
333
+ m.clip.startMs -= overlap;
334
+ m.clip.endMs -= overlap;
335
+ m.track.clips.push(m.clip);
336
+ m.track.clips.sort((x, y) => x.startMs - y.startMs);
337
+ landing(d, m.clip, facts(m.clip.id)?.picture ?? true, against.get(m.clip.id)!, skip);
338
+ }
339
+ for (const c of outgoing) setFade(fades, facts, c, 'out', holds(d, facts, c.id, b.clip.id) ? 0 : overlap);
340
+ for (const c of incoming) setFade(fades, facts, c, 'in', overlap);
341
+ return opsOf(d, fades);
342
+ }
343
+
344
+ /** Where a clip's track is, top to bottom (a track this edit opens goes in just above the one at its place). */
345
+ function rowOf(d: Draft, id: string): number {
346
+ const opened = d.opened.get(id);
347
+ return opened != null ? opened - 0.5 : findInDraft(d, id)?.track.index ?? 0;
348
+ }
349
+
350
+ /** An outgoing picture under the incoming one stays whole while that one fades in over it. */
351
+ const holds = (d: Draft, facts: FactsOf, outgoing: string, incoming: string) =>
352
+ Boolean(facts(outgoing)?.picture && facts(incoming)?.picture && rowOf(d, incoming) < rowOf(d, outgoing));
353
+
354
+ /** The white page cut in under a dip (`span`), on the track below `above` when it has room, else a new one there. */
355
+ function whiteInsert(d: Draft, src: string, span: { startMs: number; endMs: number }, above: number): Op {
356
+ const below = trackAt(d, above + 1);
357
+ const track: TrackDest = below && !below.locked && below.role !== 'audio' && free(below, span.startMs, span.endMs, new Set()) ? below.index : { insert: above + 1 };
358
+ const len = secs(span.endMs - span.startMs);
359
+ return { op: 'insert', clip: { src, time: [0, len], ...(span.startMs > 0 ? { at: secs(span.startMs) } : {}) }, track };
360
+ }
361
+
362
+ /** Take a transition away: its fades go, its clips come back against each other, its white page goes. */
363
+ export function planRemoveTransition(model: EditModel, facts: FactsOf, tr: FilmTransition, opts: TransitionOpts): TransitionPlan {
364
+ const d = draftOf(model);
365
+ const fades: Fades = new Map();
366
+ const a = findInDraft(d, tr.a), b = findInDraft(d, tr.b);
367
+ if (!a || !b) return { error: 'none' };
368
+ const outgoing = atEdge(d, model, tr.a, 'out', opts);
369
+ const incoming = atEdge(d, model, tr.b, 'in', opts);
370
+ const extra: Op[] = [];
371
+ if (tr.kind === 'dip-black' || tr.kind === 'dip-white') {
372
+ if (lockedIn(d, [...outgoing, ...incoming].map((c) => c.id)) || (tr.white && lockedIn(d, [tr.white]))) return { error: 'locked' };
373
+ if (tr.white) extra.push({ op: 'remove', clip: tr.white });
374
+ } else {
375
+ const moving = groupOf(model, tr.b, opts).map((id) => findInDraft(d, id)).filter((x): x is { track: EditTrack; clip: EditClip } => Boolean(x));
376
+ if (lockedIn(d, [...moving.map((m) => m.clip.id), ...outgoing.map((c) => c.id)])) return { error: 'locked' };
377
+ const skip = new Set(moving.map((m) => m.clip.id));
378
+ const back = a.clip.endMs - b.clip.startMs;
379
+ /* where each goes back to: against the clip it overlaps, on that one's track */
380
+ const homes = moving.map((m) => ({ m, home: partnerTrack(d, facts, m.clip, m.clip.startMs + back, skip) }));
381
+ const reached = new Set([a.track.index, ...homes.flatMap((h) => (h.home != null ? [h.home] : []))]);
382
+ if (opts.ripple) rippleThrough(d, a.clip.endMs, back, reached, skip);
383
+ for (const { m, home } of homes) {
384
+ m.clip.startMs += back;
385
+ m.clip.endMs += back;
386
+ const here = findInDraft(d, m.clip.id)!;
387
+ const to = home != null ? trackAt(d, home) : undefined;
388
+ if (to && !to.locked && free(to, m.clip.startMs, m.clip.endMs, skip)) moveTo(m.clip, here.track, to);
389
+ else if (!free(here.track, m.clip.startMs, m.clip.endMs, new Set([...skip, m.clip.id]))) {
390
+ /* no room where it was or where it came from: a track of its own, beside its own */
391
+ openTrack(d, m.clip, here.track, facts(m.clip.id)?.picture ? here.track.index : here.track.index + 1);
392
+ }
393
+ }
394
+ }
395
+ for (const c of outgoing) setFade(fades, facts, c, 'out', 0);
396
+ for (const c of incoming) setFade(fades, facts, c, 'in', 0);
397
+ return opsOf(d, fades, extra);
398
+ }
399
+
400
+ /**
401
+ * A transition made `durationMs` long: an overlap's incoming clips move by the difference (what follows with them,
402
+ * with `ripple`), a dip's fades and white page change; each as far as its clips have room.
403
+ */
404
+ export function planResizeTransition(model: EditModel, facts: FactsOf, tr: FilmTransition, durationMs: number, opts: TransitionOpts): TransitionPlan {
405
+ const d = draftOf(model);
406
+ const fades: Fades = new Map();
407
+ const a = findInDraft(d, tr.a), b = findInDraft(d, tr.b);
408
+ if (!a || !b) return { error: 'none' };
409
+ const fa = facts(tr.a)?.fade ?? [0, 0], fb = facts(tr.b)?.fade ?? [0, 0];
410
+ const outgoing = atEdge(d, model, tr.a, 'out', opts);
411
+ const incoming = atEdge(d, model, tr.b, 'in', opts);
412
+ if (tr.kind === 'dip-black' || tr.kind === 'dip-white') {
413
+ const half = Math.max(1, Math.min(Math.round(durationMs) / 2, lengthOf(a.clip) - fa[0], lengthOf(b.clip) - fb[1]));
414
+ if (lockedIn(d, [...outgoing, ...incoming].map((c) => c.id)) || (tr.white && lockedIn(d, [tr.white]))) return { error: 'locked' };
415
+ for (const c of outgoing) setFade(fades, facts, c, 'out', half);
416
+ for (const c of incoming) setFade(fades, facts, c, 'in', half);
417
+ const extra: Op[] = [];
418
+ if (tr.white) {
419
+ const startMs = a.clip.endMs - half;
420
+ extra.push({ op: 'props', edits: [{ clip: tr.white, prop: 'at', value: secs(Math.max(0, startMs)) }, { clip: tr.white, prop: 'end', value: secs(2 * half) }] });
421
+ }
422
+ return opsOf(d, fades, extra);
423
+ }
424
+ const moving = groupOf(model, tr.b, opts).map((id) => findInDraft(d, id)).filter((x): x is { track: EditTrack; clip: EditClip } => Boolean(x));
425
+ if (lockedIn(d, [...moving.map((m) => m.clip.id), ...outgoing.map((c) => c.id)])) return { error: 'locked' };
426
+ const skip = new Set(moving.map((m) => m.clip.id));
427
+ const now = a.clip.endMs - b.clip.startMs;
428
+ /* how far the incoming clips can go earlier: up to what is before each on its track, and both clips' lengths */
429
+ let most = Math.min(lengthOf(a.clip) - fa[0], lengthOf(b.clip) - fb[1]);
430
+ for (const m of moving) {
431
+ const before = m.track.clips.filter((c) => !skip.has(c.id) && c.endMs <= m.clip.startMs + EPS).reduce((end, c) => Math.max(end, c.endMs), 0);
432
+ most = Math.min(most, now + (m.clip.startMs - before));
433
+ }
434
+ const want = Math.max(1, Math.min(Math.round(durationMs), most));
435
+ const delta = want - now;
436
+ if (Math.abs(delta) < 0.5) return { ops: [], shortMs: 0 };
437
+ for (const m of moving) { m.clip.startMs -= delta; m.clip.endMs -= delta; }
438
+ if (opts.ripple) rippleThrough(d, a.clip.endMs, -delta, [a.track.index], skip);
439
+ for (const c of outgoing) setFade(fades, facts, c, 'out', holds(d, facts, c.id, tr.b) ? 0 : want);
440
+ for (const c of incoming) setFade(fades, facts, c, 'in', want);
441
+ return opsOf(d, fades);
442
+ }
443
+
444
+ /** The white page under a dip to white: white, the stage's size, still. */
445
+ export const WHITE_PAGE = `<!doctype html>
446
+ <html>
447
+ <head>
448
+ <meta charset="utf-8">
449
+ <title>White</title>
450
+ <style>html, body { margin: 0; height: 100%; background: #fff; }</style>
451
+ </head>
452
+ <body>
453
+ <script>
454
+ /* a still white picture, under a dip to white (OpenFilm Studio's Transitions) */
455
+ window.film = { frame() {} };
456
+ </script>
457
+ </body>
458
+ </html>
459
+ `;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Line height and letter spacing as the inspector takes them: "Auto", a percentage, px — and for line height a bare
3
+ * number, CSS's multiple of the size. What is written is plain CSS: `normal`, `120%`, `24px`, `1.2` for a line height;
4
+ * `normal`, `2px`, and a percentage of the size as `em` (`5%` → `0.05em`) for letter spacing.
5
+ *
6
+ * The font size menu's common sizes live here too.
7
+ */
8
+
9
+ export type MetricKind = 'line-height' | 'letter-spacing';
10
+ /** A value as the field shows it: auto, or a number in a unit (`''`: a line height's multiple). */
11
+ export type Metric = 'auto' | { n: number; unit: '' | 'px' | '%' };
12
+
13
+ /** The sizes the font size menu offers. */
14
+ export const FONT_SIZES = [12, 14, 16, 18, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 96, 120, 144, 160, 200] as const;
15
+
16
+ const round = (n: number, d = 3) => Math.round(n * 10 ** d) / 10 ** d;
17
+
18
+ /** What the page's value (a number the stage measured, or CSS) is in the field's terms. */
19
+ export function metricOf(kind: MetricKind, v: string | number | undefined | null): Metric | undefined {
20
+ if (v == null || v === '') return undefined;
21
+ if (typeof v === 'number') return Number.isFinite(v) ? { n: round(v), unit: kind === 'line-height' ? '' : 'px' } : undefined;
22
+ const s = v.trim().toLowerCase();
23
+ if (s === 'normal' || s === 'auto') return 'auto';
24
+ const m = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|%|em)?$/.exec(s);
25
+ if (!m) return undefined;
26
+ const n = Number(m[1]);
27
+ const unit = m[2] ?? '';
28
+ if (unit === 'em') return { n: round(n * 100), unit: '%' };
29
+ if (unit === '') return kind === 'line-height' ? { n, unit: '' } : { n, unit: 'px' };
30
+ return { n, unit: unit as 'px' | '%' };
31
+ }
32
+
33
+ /** The field's text for a value. */
34
+ export function showMetric(m: Metric | undefined, auto: string): string {
35
+ if (m == null) return '';
36
+ if (m === 'auto') return auto;
37
+ return `${round(m.n)}${m.unit}`;
38
+ }
39
+
40
+ /**
41
+ * What was typed, as the field's value; null when it does not read. `auto` (or the word in the person's language),
42
+ * `120%`, `24px`, `1.2`; a bare number is a multiple for a line height, px for letter spacing.
43
+ */
44
+ export function parseMetric(kind: MetricKind, text: string, autoWord = 'auto'): Metric | null {
45
+ const s = text.trim().toLowerCase().replace(/,/g, '.');
46
+ if (!s) return null;
47
+ if (s === 'auto' || s === 'normal' || s === autoWord.trim().toLowerCase()) return 'auto';
48
+ const m = /^(-?(?:\d+(?:\.\d*)?|\.\d+))\s*(px|%|em)?$/.exec(s);
49
+ if (!m) return null;
50
+ const n = Number(m[1]);
51
+ if (!Number.isFinite(n)) return null;
52
+ const unit = m[2] ?? '';
53
+ if (unit === 'em') return { n: round(n * 100), unit: '%' };
54
+ if (unit === '') return { n, unit: kind === 'line-height' ? '' : 'px' };
55
+ if (kind === 'line-height' && n <= 0) return null;
56
+ return { n, unit: unit as 'px' | '%' };
57
+ }
58
+
59
+ /** A value as CSS. */
60
+ export function metricCss(kind: MetricKind, m: Metric): string {
61
+ if (m === 'auto') return 'normal';
62
+ if (kind === 'letter-spacing' && m.unit === '%') return `${round(m.n / 100, 4)}em`;
63
+ if (kind === 'letter-spacing' && m.n === 0) return '0px';
64
+ return `${round(m.n)}${m.unit}`;
65
+ }
66
+
67
+ /** How far one step (an arrow, a few px of drag) moves a value in its unit. */
68
+ export function metricStep(m: Metric): number {
69
+ if (m === 'auto') return 0;
70
+ return m.unit === '' ? 0.05 : 1;
71
+ }
72
+
73
+ /**
74
+ * `m` moved by `steps`; from auto, from what auto is (1.2 × the size for a line height, 0 for letter spacing). A
75
+ * line height stays above 0.
76
+ */
77
+ export function stepMetric(kind: MetricKind, m: Metric | undefined, steps: number): Metric {
78
+ const base: Exclude<Metric, 'auto'> = m == null || m === 'auto'
79
+ ? (kind === 'line-height' ? { n: 1.2, unit: '' } : { n: 0, unit: 'px' })
80
+ : m;
81
+ const next = round(base.n + steps * metricStep(base), base.unit === '' ? 2 : 1);
82
+ return { n: kind === 'line-height' ? Math.max(base.unit === '' ? 0.05 : 1, next) : next, unit: base.unit };
83
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Everything the editor needs for subtitles, in one hook: the film's lines (from the server, worked out from film.html
3
+ * and the transcripts; asked again whenever the film or its files change), the person's style, and translation.
4
+ *
5
+ * cues the lines as the style shows them (picked language, or both), for SubtitleOverlay
6
+ * count how many lines the film has (0: the timeline's CC button is disabled and says why)
7
+ * style / setStyle / patchStyle / toggle / move the person's style, kept in the project folder
8
+ * translate for SubtitleInspector: translate the voices into a language, then ask for the lines again
9
+ * transcribe for the timeline's CC button: transcribe the speech that has no transcript yet
10
+ * setLine the person's correction of a line, written back into its transcript (or its translation)
11
+ * sourceLanguage the voices' language, as far as the script tells
12
+ * captions the server's answer as it is (the lines, the speech with no transcript, …)
13
+ */
14
+ import * as React from 'react';
15
+
16
+ import { projectEvents } from '@/api';
17
+ import {
18
+ fetchFilmCaptions,
19
+ filmSubtitleShown,
20
+ requestSubtitleTranscription,
21
+ requestSubtitleTranslation,
22
+ saveSubtitleLine,
23
+ type FilmCaptions,
24
+ type SubtitleCue,
25
+ type SubtitleStyle,
26
+ type SubtitleTranscribeResult,
27
+ type SubtitleTranslateResult,
28
+ } from './subtitles';
29
+ import { useSubtitleStyle } from './use-subtitle-style';
30
+
31
+ /** Changes come in bursts (an agent writes several files): one request after they settle. */
32
+ const REFRESH_MS = 250;
33
+
34
+ export interface FilmSubtitles {
35
+ cues: SubtitleCue[];
36
+ count: number;
37
+ style: SubtitleStyle;
38
+ setStyle: React.Dispatch<React.SetStateAction<SubtitleStyle>>;
39
+ patchStyle: (next: Partial<SubtitleStyle>) => void;
40
+ toggle: () => void;
41
+ move: (pos: { x: number; y: number }) => void;
42
+ translate: (language: string) => Promise<SubtitleTranslateResult>;
43
+ transcribe: () => Promise<SubtitleTranscribeResult>;
44
+ /** Whether a transcription runs now. */
45
+ transcribing: boolean;
46
+ /** with a `language`, the line's translation into it */
47
+ setLine: (cue: SubtitleCue, text: string, language?: string) => Promise<boolean>;
48
+ sourceLanguage: string | null;
49
+ captions: FilmCaptions | null;
50
+ refresh: () => Promise<void>;
51
+ }
52
+
53
+ export function useFilmSubtitles(projectId: string): FilmSubtitles {
54
+ const [captions, setCaptions] = React.useState<FilmCaptions | null>(null);
55
+ const [style, setStyle] = useSubtitleStyle(projectId);
56
+ const [transcribing, setTranscribing] = React.useState(false);
57
+
58
+ const latest = React.useRef(0);
59
+ const refresh = React.useCallback(async () => {
60
+ const ask = ++latest.current;
61
+ try {
62
+ const next = await fetchFilmCaptions(projectId);
63
+ if (ask === latest.current) setCaptions(next);
64
+ } catch { /* keep what is shown; the next change asks again */ }
65
+ }, [projectId]);
66
+
67
+ React.useEffect(() => {
68
+ setCaptions(null);
69
+ void refresh();
70
+ let timer: ReturnType<typeof setTimeout> | null = null;
71
+ const stop = projectEvents(projectId, (event) => {
72
+ if (event.type !== 'film' && event.type !== 'files') return;
73
+ if (timer) clearTimeout(timer);
74
+ timer = setTimeout(() => { timer = null; void refresh(); }, REFRESH_MS);
75
+ });
76
+ return () => { stop(); if (timer) clearTimeout(timer); };
77
+ }, [projectId, refresh]);
78
+
79
+ const cues = React.useMemo(() => filmSubtitleShown(captions?.cues ?? [], style), [captions, style]);
80
+ const count = React.useMemo(() => (captions?.cues ?? []).filter((c) => c.durMs > 0).length, [captions]);
81
+ const patchStyle = React.useCallback((next: Partial<SubtitleStyle>) => setStyle((v) => ({ ...v, ...next })), [setStyle]);
82
+ const toggle = React.useCallback(() => setStyle((v) => ({ ...v, on: !v.on })), [setStyle]);
83
+ const move = React.useCallback((pos: { x: number; y: number }) => setStyle((v) => ({ ...v, pos })), [setStyle]);
84
+ const translate = React.useCallback(async (language: string) => {
85
+ const result = await requestSubtitleTranslation(projectId, language);
86
+ /* ask again even when nothing new came: lines may still lack translations made before */
87
+ await refresh();
88
+ return result;
89
+ }, [projectId, refresh]);
90
+
91
+ const transcribe = React.useCallback(async () => {
92
+ setTranscribing(true);
93
+ try {
94
+ const result = await requestSubtitleTranscription(projectId);
95
+ await refresh();
96
+ if (result.ok) setStyle((v) => ({ ...v, on: true }));
97
+ return result;
98
+ } finally { setTranscribing(false); }
99
+ }, [projectId, refresh, setStyle]);
100
+ const setLine = React.useCallback(async (cue: SubtitleCue, text: string, language?: string) => {
101
+ if (cue.src === undefined || cue.line === undefined) return false;
102
+ const ok = await saveSubtitleLine(projectId, { src: cue.src, line: cue.line, ...(cue.part !== undefined ? { part: cue.part } : {}), text, ...(language ? { language } : {}) });
103
+ await refresh();
104
+ return ok;
105
+ }, [projectId, refresh]);
106
+
107
+ return {
108
+ cues, count, style, setStyle, patchStyle, toggle, move, translate, transcribe, transcribing, setLine,
109
+ sourceLanguage: captions?.sourceLanguage ?? null, captions, refresh,
110
+ };
111
+ }