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.
- package/LICENSE +21 -0
- package/README.md +29 -2
- package/bin/install.mjs +66 -0
- package/package.json +29 -4
- package/skill/SKILL.md +64 -0
- package/skill/agents/openai.yaml +4 -0
- package/skill/assets/examples/ApprovalCard.tsx +8 -0
- package/skill/assets/examples/BasicApp.tsx +38 -0
- package/skill/assets/examples/BriefingCard.tsx +8 -0
- package/skill/assets/examples/ChatControls.tsx +49 -0
- package/skill/assets/examples/ChatPanel.tsx +73 -0
- package/skill/assets/examples/Checkbox.tsx +13 -0
- package/skill/assets/examples/Combobox.tsx +14 -0
- package/skill/assets/examples/Drawer.tsx +10 -0
- package/skill/assets/examples/MemoryCard.tsx +9 -0
- package/skill/assets/examples/MessageList.tsx +8 -0
- package/skill/assets/examples/Theme.tsx +14 -0
- package/skill/assets/examples/ToolCall.tsx +8 -0
- package/skill/assets/examples/Tree.tsx +15 -0
- package/skill/assets/examples/examples.css +11 -0
- package/skill/assets/library/auricui-0.1.0-preview.1.tgz +0 -0
- package/skill/assets/library/auricui-0.1.0-preview.3.tgz +0 -0
- package/skill/assets/library/auricui-0.1.0-preview.4.tgz +0 -0
- package/skill/assets/upstream/LICENSE +21 -0
- package/skill/assets/upstream/apps/desktop/chat/src/App.tsx +1099 -0
- package/skill/assets/upstream/apps/desktop/chat/src/app-bridge.ts +154 -0
- package/skill/assets/upstream/apps/desktop/chat/src/chat.css +1812 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AddSkill.tsx +223 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AgentActivityRow.tsx +716 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AgentIcons.tsx +46 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentCard.tsx +34 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentPreview.tsx +331 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/BalanceNotice.tsx +22 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/BrandLogo.tsx +35 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ComposerCard.tsx +182 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ConversationSessionBar.tsx +292 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/CopyDiagnostics.tsx +24 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/Divider.tsx +47 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/FileChangesList.tsx +119 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/Markdown.tsx +198 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ModelPopover.tsx +280 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/NoticeCard.tsx +87 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/Popover.tsx +195 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/PreviewFrame.tsx +129 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectConversation.tsx +852 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectStarters.tsx +118 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/Tooltip.tsx +191 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/TurnLog.tsx +583 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/TurnQuestionCard.tsx +133 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/dock-pane-bar.ts +14 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/project-version.ts +37 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/MentionMenuPlugin.tsx +372 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptEditor.tsx +476 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptPill.tsx +399 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptReferenceNode.tsx +164 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/index.ts +31 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/mention-menu.ts +206 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/prompt-text.ts +171 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/useStickToBottom.ts +320 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/useTicker.ts +55 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/index.ts +42 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/en-US.ts +408 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/es.ts +407 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ja.ts +407 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ko.ts +403 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/words.ts +6 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/zh-CN.ts +407 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/lookup.ts +12 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-activity-runs.ts +154 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-menu-rows.ts +41 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-refs.ts +105 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-transcript.ts +440 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/asset-lists.ts +75 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/attachment-place.ts +17 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/balance.ts +16 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-layer.ts +10 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-menu.ts +18 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-store.ts +101 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/composer-attachments.ts +189 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-attachments.ts +178 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-bridge.ts +291 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/developer.ts +39 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/esc-stop.ts +23 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/file-changes.ts +32 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/live-activity.ts +20 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/local-agent-run.ts +1174 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/local-sessions.ts +25 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/model-calls.ts +87 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/pretty-json.ts +12 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/preview-top.ts +15 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/project-resources.ts +66 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/project-sessions.ts +66 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/project-versions.ts +116 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/ref-pictures.ts +119 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/relative-time.ts +46 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/skill-pages.ts +29 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/step-file.ts +23 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/structural-share.ts +52 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-film.ts +44 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-transfer.ts +40 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/unprompted.ts +27 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/upload-limits.ts +53 -0
- package/skill/assets/upstream/apps/desktop/chat/src/mount.tsx +19 -0
- package/skill/assets/upstream/apps/desktop/chat/src/state/local-agent-store.ts +296 -0
- package/skill/assets/upstream/apps/desktop/chat/src/studio-refs.ts +307 -0
- package/skill/assets/upstream/apps/desktop/package.json +51 -0
- package/skill/assets/upstream/apps/desktop/shell/bridge.ts +415 -0
- package/skill/assets/upstream/apps/desktop/shell/index.html +27 -0
- package/skill/assets/upstream/apps/desktop/shell/main.tsx +17 -0
- package/skill/assets/upstream/apps/desktop/shell/shell.css +17 -0
- package/skill/assets/upstream/apps/desktop/shell/vite.config.ts +65 -0
- package/skill/assets/upstream/apps/desktop/src/agent-definitions.mjs +31 -0
- package/skill/assets/upstream/packages/openfilm/LICENSE +21 -0
- package/skill/assets/upstream/packages/openfilm/package.json +88 -0
- package/skill/assets/upstream/packages/openfilm/src/film-doc.d.mts +125 -0
- package/skill/assets/upstream/packages/openfilm/src/film-doc.mjs +986 -0
- package/skill/assets/upstream/packages/openfilm/src/overrides.mjs +73 -0
- package/skill/assets/upstream/packages/openfilm/src/timeline.mjs +1280 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/bridge.js +209 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/film-subtitle.mjs +785 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/ops.mjs +403 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/stage.js +1649 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/track-layout.mjs +43 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/App.tsx +156 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/api.ts +328 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/AudioMeters.tsx +188 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockThumbs.tsx +84 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockWaveform.tsx +201 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLoadingScreen.tsx +56 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLogo.tsx +25 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Card.tsx +89 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ColorPicker.tsx +204 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ConfirmDialog.tsx +96 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ContextMenu.tsx +422 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/DesktopDialog.tsx +271 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ExamplesGallery.tsx +372 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmFullscreenBar.tsx +147 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmInspector.tsx +2310 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmStageSelect.tsx +3296 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FocusNav.tsx +28 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FontFamilyPicker.tsx +293 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FramePicker.tsx +69 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/InspectorPanel.tsx +25 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Launcher.tsx +38 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LayersPanel.tsx +277 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LiveTime.tsx +101 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPool.tsx +1413 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPreview.tsx +188 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MenuSelect.tsx +161 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MgOutline.tsx +24 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/OperationHistory.tsx +184 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PaneDivider.tsx +292 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PasteAttrsDialog.tsx +69 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PlaybackMenu.tsx +110 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Popover.tsx +159 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectCover.tsx +60 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectHistoryControls.tsx +748 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectLayoutMenu.tsx +82 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectMenu.tsx +111 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectSources.tsx +587 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectView.tsx +3222 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsLibrary.tsx +567 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsView.tsx +1593 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PropertiesSection.tsx +418 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ResourceInspector.tsx +100 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SelectionToolbar.tsx +157 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SkillsLibrary.tsx +504 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SourcePreview.tsx +464 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SpendAsks.tsx +88 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageMaskOverlay.tsx +299 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageTransformFrame.tsx +246 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.module.css +46 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.tsx +884 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleOverlay.tsx +148 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleRow.tsx +419 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TabStrip.tsx +188 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Timeline.tsx +4911 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TimelineMarkers.tsx +329 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Toast.tsx +55 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Tooltip.tsx +193 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/UpdateButton.tsx +104 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ViewerBar.tsx +98 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/WhatsNew.tsx +147 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/dock-pane-bar.ts +16 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportCenter.tsx +1502 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportTray.tsx +284 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/film-stage-hit.ts +18 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-fields.tsx +863 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-look.tsx +1357 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/controls-bar.tsx +397 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/transport.tsx +294 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/replace-drop.ts +20 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/AgentsSection.tsx +613 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/CustomServices.tsx +104 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/DiagnosticsGroup.tsx +202 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ModelsSection.tsx +179 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProviderMark.tsx +24 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProvidersSection.tsx +177 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsModal.tsx +511 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsSelect.tsx +84 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ShortcutsSection.tsx +291 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/StorageGroup.tsx +119 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/theme.ts +47 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ui.tsx +139 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/use-settings.ts +10 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/stage-menu.tsx +283 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/timeline-head-extras.tsx +91 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/typing-target.ts +45 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-editor-tools.tsx +408 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-page-changes.ts +48 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-project-history.ts +113 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/useBlockDrag.ts +639 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/Preview.tsx +426 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/player.ts +558 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/preview-frames.ts +36 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/steps.ts +36 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/undo.ts +156 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-film.ts +212 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-preview-renders.ts +89 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/globals.css +559 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/en.ts +1901 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/es.ts +1900 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/index.ts +130 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ja.ts +1900 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ko.ts +1896 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/words.ts +6 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/zh-CN.ts +1901 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/agent-row.ts +21 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/asset-timeline.ts +469 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/audio-levels.ts +60 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/batch-map.ts +28 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/browser.ts +10 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/canvas-fit.ts +116 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/chat-marks.ts +114 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-attrs.ts +102 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-fade.ts +70 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-look.ts +520 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-mask.ts +536 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-replace.ts +60 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/component-props.ts +128 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/context-length.ts +12 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/desktop-build.ts +44 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/diagnostics.ts +68 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/double-press.ts +22 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/dropped-overrides.ts +139 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-jobs.ts +191 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-spec.ts +261 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/field-input.ts +155 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-clip-edits.ts +82 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-frame.ts +134 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-overrides.ts +107 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-shape.ts +172 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film.ts +80 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-menu.ts +236 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-styles.ts +129 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/freeze-frame.ts +49 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/host.ts +438 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/inspect-scrub.ts +26 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap-store.ts +74 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap.ts +529 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/layers-tree.ts +133 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/look.ts +83 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/loudness.ts +58 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/markers.ts +152 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-bar.ts +58 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-folders.ts +45 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-place.ts +102 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-queue.ts +107 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-selection.ts +64 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/menu-position.ts +77 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mg-transform.ts +111 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mixed.ts +3 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/page-changes.ts +45 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/picture-files.ts +19 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-prefs.ts +46 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-renders.ts +71 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-delete.ts +20 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-folders.ts +144 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-history.ts +188 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-sources.ts +283 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/projects-page.ts +40 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/provider-marks.ts +35 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/providers.ts +163 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/resource-drag.ts +131 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/retry-image.ts +17 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved-insert.ts +40 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved.ts +125 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/selection-toolbar.ts +158 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/shortcut-hint.ts +21 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/similar-layers.ts +46 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/solo.ts +19 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-clip.ts +60 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-crop.ts +232 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-gesture.ts +277 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-layers.ts +71 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-mask.ts +176 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-measure.ts +53 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-types.ts +155 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-refs.ts +248 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-version.ts +41 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitle-cues.ts +238 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitles.ts +263 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/tab-menu.ts +6 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/template-controls.ts +131 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/theme-changes.ts +31 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cache.ts +63 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cell.ts +8 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-frame.ts +76 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-pick.ts +42 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timecode.ts +161 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-clipboard.ts +279 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-drag.ts +589 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-edit.ts +1138 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-keys.ts +63 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-layout.ts +1038 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-nav.ts +160 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-prefs.ts +73 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-split.ts +40 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-view.ts +105 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/title.ts +39 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-heights.ts +55 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-lock.ts +27 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/transitions.ts +459 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/type-metrics.ts +83 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-film-subtitles.ts +111 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-fonts.ts +181 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-loaded-thumbs.ts +91 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-edits.ts +134 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-font.ts +51 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-style.ts +68 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/waveform-columns.ts +48 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/web-update.ts +41 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/whats-new.ts +401 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/workspace-resources.ts +373 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/main.tsx +10 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/vite.config.ts +12 -0
- package/skill/assets/upstream/packages/shared/package.json +45 -0
- package/skill/assets/upstream/packages/shared/src/agent-activity.ts +263 -0
- package/skill/assets/upstream/packages/shared/src/contract.ts +285 -0
- package/skill/assets/upstream/packages/shared/src/index.ts +17 -0
- package/skill/assets/upstream/packages/shared/src/markdown-blocks.ts +252 -0
- package/skill/assets/upstream/packages/shared/src/modality.ts +4 -0
- package/skill/assets/upstream/packages/shared/src/prompt-reference.ts +113 -0
- package/skill/assets/upstream/packages/shared/src/title.ts +76 -0
- package/skill/assets/upstream/packages/shared/src/turn.ts +68 -0
- package/skill/assets/upstream/packages/shared/src/video.ts +26 -0
- package/skill/references/agent-components.md +33 -0
- package/skill/references/api-index.json +2693 -0
- package/skill/references/chat-components.md +65 -0
- package/skill/references/components.md +23 -0
- package/skill/references/core-components.md +44 -0
- package/skill/references/dependencies.md +22 -0
- package/skill/references/esm-package.md +48 -0
- package/skill/references/integration.md +43 -0
- package/skill/references/library-manifest.json +12 -0
- package/skill/references/source-manifest.json +325 -0
- package/skill/references/studio-components.md +24 -0
- package/skill/references/theme-motion.md +49 -0
- package/skill/references/troubleshooting.md +27 -0
- package/skill/references/verification.md +13 -0
- package/skill/scripts/check_dependencies.py +109 -0
- package/skill/scripts/inspect_api.py +60 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor's audio and timeline tools, gathered for the project's page (ProjectView): markers (lib/markers), solo
|
|
3
|
+
* (lib/solo), track heights (lib/track-heights), loudness normalization (lib/loudness), freeze frame
|
|
4
|
+
* (lib/freeze-frame) and replacing a clip (lib/clip-replace). What the timeline draws and its menus offer come from
|
|
5
|
+
* here; the edits go through the page's own edit, one write and one undo step each.
|
|
6
|
+
*/
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import { AudioLines, HardDriveUpload, Replace, Snowflake } from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
import { api, type Clip, type Film, type Op, type ProjectSettings } from '@/api';
|
|
11
|
+
import { useT } from '@/i18n';
|
|
12
|
+
import { canReplace, familyOfClip, replaceClip, type ReplaceFamily, type ReplaceFile } from '@/lib/clip-replace';
|
|
13
|
+
import { freezeSourceMs, planFreeze } from '@/lib/freeze-frame';
|
|
14
|
+
import { LOUDNESS_TARGETS, measuredSpan, normalizeVolume } from '@/lib/loudness';
|
|
15
|
+
import {
|
|
16
|
+
addMarker, changeMarker, markerClip, nextMarker, parseMarkers, placeMarkers, removeMarker, type Marker, type PlacedMarker,
|
|
17
|
+
} from '@/lib/markers';
|
|
18
|
+
import type { ClipEdit } from '@/lib/film-clip-edits';
|
|
19
|
+
import { heardClips, toggleSolo } from '@/lib/solo';
|
|
20
|
+
import type { EditModel } from '@/lib/timeline-edit';
|
|
21
|
+
import { readTimelinePrefs, switchesOf } from '@/lib/timeline-prefs';
|
|
22
|
+
import type { ActionId } from '@/lib/keymap';
|
|
23
|
+
import { useKeyHint } from '@/lib/keymap-store';
|
|
24
|
+
import type { TimelineBlock, TimelineTrack } from '@/lib/timeline-layout';
|
|
25
|
+
import type { MediaListing } from '@/lib/workspace-resources';
|
|
26
|
+
import { ownTrackHeight, sizeOf, trackHeightsOf, withTrackHeight, TRACK_SIZES, type TrackSize } from '@/lib/track-heights';
|
|
27
|
+
import type { Player } from '@/editor/player';
|
|
28
|
+
import type { OutsideStep } from '@/editor/use-film';
|
|
29
|
+
import type { ContextMenuEntry } from './ContextMenu';
|
|
30
|
+
import type { MarkerLaneView } from './Timeline';
|
|
31
|
+
import { ClipMarkers, LaneMarkers, MarkerGlyph, type MarkerActions } from './TimelineMarkers';
|
|
32
|
+
|
|
33
|
+
type Toast = { show: (text: string) => void; showError: (text: string) => void };
|
|
34
|
+
|
|
35
|
+
/** A file brought in to replace a clip: how long its length is waited for, and how long the file itself. */
|
|
36
|
+
const REPLACE_PROBE_MS = 8000;
|
|
37
|
+
const REPLACE_GIVE_UP_MS = 30_000;
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/** Whether a clip on the timeline is heard: a sound, or a video with a sound of its own that is not turned off. */
|
|
41
|
+
const sounding = (b: TimelineBlock) => Boolean(b.clipId && b.loc && b.src) && (
|
|
42
|
+
b.kind === 'voice' || b.kind === 'sfx' || b.kind === 'music' || (b.kind === 'video' && b.ownAudio && !b.silent)
|
|
43
|
+
) && b.volume !== 0;
|
|
44
|
+
|
|
45
|
+
export function useEditorTools({
|
|
46
|
+
projectId, tracks, film, settings, patchSettings, player, timeRef, seek, edit, record, editModel, selectedClipIds, listing, toast, pickFile,
|
|
47
|
+
}: {
|
|
48
|
+
projectId: string;
|
|
49
|
+
tracks: readonly TimelineTrack[];
|
|
50
|
+
film: Film | null;
|
|
51
|
+
settings: ProjectSettings | null;
|
|
52
|
+
patchSettings: (patch: Parameters<typeof api.patchSettings>[1]) => void;
|
|
53
|
+
player: React.MutableRefObject<Player>;
|
|
54
|
+
/** where the playhead is, ms */
|
|
55
|
+
timeRef: React.MutableRefObject<number>;
|
|
56
|
+
seek: (ms: number) => void;
|
|
57
|
+
edit: (ops: Op[], label: string) => Promise<string | null>;
|
|
58
|
+
/** Keeps an edit of another file (the markers) as a step of the film's history, so ⌘Z undoes it in turn. */
|
|
59
|
+
record: (label: string, outside: OutsideStep) => void;
|
|
60
|
+
editModel: () => EditModel;
|
|
61
|
+
selectedClipIds: readonly string[];
|
|
62
|
+
/** the project's media and pages, with their lengths as probed */
|
|
63
|
+
listing: MediaListing | null;
|
|
64
|
+
toast: Toast;
|
|
65
|
+
/**
|
|
66
|
+
* A file from this computer, to replace a clip with: the person picks it, it is brought into the project, and its
|
|
67
|
+
* path there comes back (null: cancelled). Missing: the replace menu has only the project's files.
|
|
68
|
+
*/
|
|
69
|
+
pickFile?: (family: ReplaceFamily) => Promise<string | null>;
|
|
70
|
+
}) {
|
|
71
|
+
const t = useT();
|
|
72
|
+
const keyHint = useKeyHint();
|
|
73
|
+
/* what can take a clip's place: the media and the pages */
|
|
74
|
+
const media = React.useMemo<readonly (ReplaceFile & { name: string })[]>(
|
|
75
|
+
() => [...(listing?.files ?? []).filter((f) => f.kind !== 'other'), ...(listing?.pages ?? [])],
|
|
76
|
+
[listing],
|
|
77
|
+
);
|
|
78
|
+
const tracksRef = React.useRef(tracks);
|
|
79
|
+
tracksRef.current = tracks;
|
|
80
|
+
const filmRef = React.useRef(film);
|
|
81
|
+
filmRef.current = film;
|
|
82
|
+
|
|
83
|
+
/* ── markers: read once per project, written whole on each change (one at a time, in order), each change a step
|
|
84
|
+
of the history (⌘Z puts the list back as it was) ── */
|
|
85
|
+
const [markers, setMarkers] = React.useState<Marker[]>([]);
|
|
86
|
+
const markersRef = React.useRef(markers);
|
|
87
|
+
markersRef.current = markers;
|
|
88
|
+
const writes = React.useRef<Promise<unknown>>(Promise.resolve());
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
let live = true;
|
|
91
|
+
setMarkers([]);
|
|
92
|
+
void api.markers(projectId).then((raw) => { if (live) setMarkers(parseMarkers(raw)); }, () => {});
|
|
93
|
+
return () => { live = false; };
|
|
94
|
+
}, [projectId]);
|
|
95
|
+
const writeMarkers = React.useCallback((next: Marker[]): Promise<string | null> => {
|
|
96
|
+
setMarkers(next);
|
|
97
|
+
markersRef.current = next;
|
|
98
|
+
const done = writes.current.then(() => api.putMarkers(projectId, next)).then(() => null, (e: Error) => e.message);
|
|
99
|
+
writes.current = done;
|
|
100
|
+
return done;
|
|
101
|
+
}, [projectId]);
|
|
102
|
+
const saveMarkers = React.useCallback((next: Marker[]) => {
|
|
103
|
+
const was = markersRef.current;
|
|
104
|
+
void writeMarkers(next).then((err) => { if (err) toast.showError(err); });
|
|
105
|
+
record('history.stepMarker', { undo: () => writeMarkers(was), redo: () => writeMarkers(next) });
|
|
106
|
+
}, [writeMarkers, record, toast]);
|
|
107
|
+
const placed = React.useMemo(() => placeMarkers(markers, tracks), [markers, tracks]);
|
|
108
|
+
const [naming, setNaming] = React.useState<string | null>(null);
|
|
109
|
+
const onNamed = React.useCallback(() => setNaming(null), []);
|
|
110
|
+
const placedRef = React.useRef(placed);
|
|
111
|
+
placedRef.current = placed;
|
|
112
|
+
const snapPointsMs = React.useMemo(() => placed.map((p) => p.ms), [placed]);
|
|
113
|
+
|
|
114
|
+
/* `atMs`: where (the playhead when not said) */
|
|
115
|
+
const addMarkerHere = React.useCallback((on?: TimelineBlock | null, at?: number) => {
|
|
116
|
+
const atMs = at ?? timeRef.current;
|
|
117
|
+
const clip = on === undefined ? markerClip(tracksRef.current, selectedClipIds, atMs) : on;
|
|
118
|
+
const { list, added } = addMarker(markersRef.current, atMs, clip);
|
|
119
|
+
/* one there already: M again names it */
|
|
120
|
+
if (list.length === markersRef.current.length) { setNaming(added.id); return; }
|
|
121
|
+
saveMarkers(list);
|
|
122
|
+
if (added.clip && clip) toast.show(t('editorTools.markerAddedOn').replace('{clip}', clip.title));
|
|
123
|
+
}, [selectedClipIds, saveMarkers, timeRef, toast, t]);
|
|
124
|
+
const goMarker = React.useCallback((dir: 1 | -1) => {
|
|
125
|
+
const to = nextMarker(placedRef.current, timeRef.current, dir);
|
|
126
|
+
if (to) seek(to.ms);
|
|
127
|
+
}, [seek, timeRef]);
|
|
128
|
+
const markerActions = React.useMemo<MarkerActions>(() => ({
|
|
129
|
+
onSeek: (ms) => seek(ms),
|
|
130
|
+
onChange: (id, change) => {
|
|
131
|
+
const m = markersRef.current.find((x) => x.id === id);
|
|
132
|
+
/* a clip marker moved is moved within its clip's file */
|
|
133
|
+
if (m?.clip && change.t !== undefined) {
|
|
134
|
+
const p = placedRef.current.find((x) => x.marker.id === id);
|
|
135
|
+
if (p?.clip) change = { ...change, t: m.t + ((change.t * 1000 - p.ms) * (p.clip.speed ?? 1)) / 1000 };
|
|
136
|
+
}
|
|
137
|
+
saveMarkers(changeMarker(markersRef.current, id, change));
|
|
138
|
+
},
|
|
139
|
+
onRemove: (id) => saveMarkers(removeMarker(markersRef.current, id)),
|
|
140
|
+
naming,
|
|
141
|
+
onNamed,
|
|
142
|
+
}), [seek, saveMarkers, naming, onNamed]);
|
|
143
|
+
|
|
144
|
+
/** The editor's keys for markers (lib/keymap): true when it was one. */
|
|
145
|
+
const onKey = React.useCallback((cmd: ActionId): boolean => {
|
|
146
|
+
if (cmd === 'add-marker') { addMarkerHere(); return true; }
|
|
147
|
+
if (cmd === 'next-marker' || cmd === 'prev-marker') { goMarker(cmd === 'next-marker' ? 1 : -1); return true; }
|
|
148
|
+
return false;
|
|
149
|
+
}, [addMarkerHere, goMarker]);
|
|
150
|
+
|
|
151
|
+
/* ── solo: Studio's playback only ── */
|
|
152
|
+
const [solo, setSolo] = React.useState<ReadonlySet<number>>(new Set());
|
|
153
|
+
React.useEffect(() => { setSolo(new Set()); }, [projectId]);
|
|
154
|
+
const heard = React.useMemo(() => heardClips(film?.tracks ?? [], solo), [film, solo]);
|
|
155
|
+
React.useEffect(() => { player.current.setHeard(heard); }, [player, heard]);
|
|
156
|
+
const soloProp = React.useMemo(() => ({
|
|
157
|
+
tracks: solo,
|
|
158
|
+
onToggle: (docIndex: number, only: boolean) => setSolo((now) => toggleSolo(now, docIndex, only)),
|
|
159
|
+
}), [solo]);
|
|
160
|
+
|
|
161
|
+
/* ── track heights: kept in the project's settings; while an edge is dragged, the height in hand ── */
|
|
162
|
+
const [live, setLive] = React.useState<{ docIndex: number; height: number } | null>(null);
|
|
163
|
+
React.useEffect(() => { setLive(null); }, [settings]);
|
|
164
|
+
const heights = React.useMemo(() => trackHeightsOf(settings?.trackHeights ?? [], film?.tracks ?? []), [settings, film]);
|
|
165
|
+
const shownTracks = React.useMemo(() => tracks.map((tr) => {
|
|
166
|
+
const h = tr.docIndex == null ? undefined : live?.docIndex === tr.docIndex ? live.height : heights.get(tr.docIndex);
|
|
167
|
+
return h == null ? tr : { ...tr, height: h };
|
|
168
|
+
}), [tracks, heights, live]);
|
|
169
|
+
const setHeights = React.useCallback((indexes: readonly number[], height: number | null) => {
|
|
170
|
+
patchSettings({ trackHeights: withTrackHeight(settings?.trackHeights ?? [], filmRef.current?.tracks ?? [], indexes, height) });
|
|
171
|
+
}, [settings, patchSettings]);
|
|
172
|
+
const onResizeTrack = React.useCallback((track: TimelineTrack, height: number | null, done: boolean) => {
|
|
173
|
+
if (track.docIndex == null) return;
|
|
174
|
+
if (!done && height != null) { setLive({ docIndex: track.docIndex, height }); return; }
|
|
175
|
+
/* back to its own height, or the one dragged to (the timeline's own one is not kept) */
|
|
176
|
+
setHeights([track.docIndex], height == null || height === ownTrackHeight(track) ? null : height);
|
|
177
|
+
}, [setHeights]);
|
|
178
|
+
const headMenuExtra = React.useCallback((track: TimelineTrack): ContextMenuEntry[] => {
|
|
179
|
+
const docIndex = track.docIndex;
|
|
180
|
+
if (docIndex == null) return [];
|
|
181
|
+
const now = heights.get(docIndex) ?? ownTrackHeight(track);
|
|
182
|
+
const size = sizeOf(now, track.kind);
|
|
183
|
+
const sizes = (Object.keys(TRACK_SIZES) as TrackSize[]);
|
|
184
|
+
const all = filmRef.current?.tracks.map((_, i) => i) ?? [];
|
|
185
|
+
const heightFor = (s: TrackSize, kind: 'visual' | 'audio') => (s === 'medium' ? null : TRACK_SIZES[s][kind]);
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
id: 'track-height',
|
|
189
|
+
label: t('editorTools.trackHeight'),
|
|
190
|
+
submenu: sizes.map((s) => ({
|
|
191
|
+
id: `height:${s}`, label: t(`editorTools.size.${s}`), checked: size === s,
|
|
192
|
+
onSelect: () => setHeights([docIndex], heightFor(s, track.kind)),
|
|
193
|
+
})),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'tracks-height',
|
|
197
|
+
label: t('editorTools.allTracksHeight'),
|
|
198
|
+
submenu: sizes.map((s) => ({
|
|
199
|
+
id: `heights:${s}`, label: t(`editorTools.size.${s}`),
|
|
200
|
+
onSelect: () => {
|
|
201
|
+
/* each track its kind's size of it; medium is the timeline's own, nothing to keep */
|
|
202
|
+
if (s === 'medium') { patchSettings({ trackHeights: null }); return; }
|
|
203
|
+
const rows = tracksRef.current;
|
|
204
|
+
const kindOf = (i: number) => rows.find((r) => r.docIndex === i)?.kind ?? 'visual';
|
|
205
|
+
let saved: NonNullable<ProjectSettings['trackHeights']> = [];
|
|
206
|
+
for (const kind of ['visual', 'audio'] as const) {
|
|
207
|
+
saved = withTrackHeight(saved, filmRef.current?.tracks ?? [], all.filter((i) => kindOf(i) === kind), TRACK_SIZES[s][kind]);
|
|
208
|
+
}
|
|
209
|
+
patchSettings({ trackHeights: saved });
|
|
210
|
+
},
|
|
211
|
+
})),
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}, [heights, setHeights, patchSettings, t]);
|
|
215
|
+
|
|
216
|
+
/* ── loudness ── */
|
|
217
|
+
const [measuring, setMeasuring] = React.useState(false);
|
|
218
|
+
const normalize = React.useCallback(async (blocks: readonly TimelineBlock[], target: number) => {
|
|
219
|
+
const heardBlocks = blocks.filter(sounding);
|
|
220
|
+
if (!heardBlocks.length) { toast.show(t('editorTools.normalizeNone')); return; }
|
|
221
|
+
if (heardBlocks.some((b) => tracksRef.current.some((tr) => tr.locked && tr.blocks.some((x) => x.id === b.id)))) { toast.show(t('timeline.trackLocked')); return; }
|
|
222
|
+
setMeasuring(true);
|
|
223
|
+
toast.show(t('editorTools.normalizing'));
|
|
224
|
+
try {
|
|
225
|
+
const measured = await Promise.all(heardBlocks.map(async (b) => {
|
|
226
|
+
const { from, to } = measuredSpan(b);
|
|
227
|
+
try { return { b, lufs: (await api.loudness(projectId, b.src!, from, to)).lufs, error: null as string | null }; }
|
|
228
|
+
catch (e) { return { b, lufs: null, error: (e as Error).message }; }
|
|
229
|
+
}));
|
|
230
|
+
const edits: ClipEdit[] = [];
|
|
231
|
+
const notes: string[] = [];
|
|
232
|
+
for (const { b, lufs, error } of measured) {
|
|
233
|
+
if (error) { notes.push(t('editorTools.normalizeFail').replace('{clip}', b.title).replace('{reason}', error)); continue; }
|
|
234
|
+
const plan = normalizeVolume(lufs, target);
|
|
235
|
+
if ('silent' in plan) { notes.push(t('editorTools.normalizeSilent').replace('{clip}', b.title)); continue; }
|
|
236
|
+
edits.push({ loc: b.loc!, prop: 'volume', value: plan.volume === 1 ? null : plan.volume });
|
|
237
|
+
if (!plan.reached) notes.push(t('editorTools.normalizeShort').replace('{clip}', b.title).replace('{target}', String(target)).replace('{lufs}', String(plan.lufs)));
|
|
238
|
+
else if (heardBlocks.length === 1) notes.push(t('editorTools.normalizedOne').replace('{clip}', b.title).replace('{from}', String(lufs)).replace('{target}', String(target)).replace('{pct}', String(Math.round(plan.volume * 100))));
|
|
239
|
+
}
|
|
240
|
+
if (edits.length) {
|
|
241
|
+
const ops: Op[] = [{ op: 'props', edits: edits.map((e) => ({ clip: heardBlocks.find((b) => b.loc === e.loc)!.clipId!, prop: 'volume', value: e.value as number | null })) }];
|
|
242
|
+
const failed = await edit(ops, `history.stepNormalized|${target}`);
|
|
243
|
+
if (failed) { toast.showError(failed); return; }
|
|
244
|
+
}
|
|
245
|
+
const done = edits.length > 1 ? [t('editorTools.normalized').replace('{n}', String(edits.length)).replace('{target}', String(target))] : [];
|
|
246
|
+
if (done.length || notes.length) toast.show([...done, ...notes].join(' · '));
|
|
247
|
+
} finally {
|
|
248
|
+
setMeasuring(false);
|
|
249
|
+
}
|
|
250
|
+
}, [projectId, edit, toast, t]);
|
|
251
|
+
|
|
252
|
+
/* ── freeze frame ── */
|
|
253
|
+
const freeze = React.useCallback(async (block: TimelineBlock) => {
|
|
254
|
+
const at = timeRef.current;
|
|
255
|
+
const srcMs = freezeSourceMs(block, at);
|
|
256
|
+
const ti = filmRef.current?.tracks.findIndex((tr) => tr.clips.some((c) => c.id === block.clipId)) ?? -1;
|
|
257
|
+
const clip = ti >= 0 ? filmRef.current!.tracks[ti]!.clips.find((c) => c.id === block.clipId) : undefined;
|
|
258
|
+
if (srcMs == null || !clip || !block.src) { toast.show(t('editorTools.freezeOutside')); return; }
|
|
259
|
+
if (filmRef.current!.tracks[ti]!.locked) { toast.show(t('timeline.trackLocked')); return; }
|
|
260
|
+
let still: string;
|
|
261
|
+
try { still = await api.freezeFrame(projectId, block.src, srcMs); }
|
|
262
|
+
catch (e) { toast.showError(t('editorTools.freezeFail').replace('{reason}', (e as Error).message)); return; }
|
|
263
|
+
/* the still goes in between at the playhead: what follows on that track moves on (with linkage, what
|
|
264
|
+
rides on the main track along) */
|
|
265
|
+
const ops = planFreeze(editModel(), { clip, track: ti, atMs: at, still, mode: 'insert', ...switchesOf(readTimelinePrefs()) });
|
|
266
|
+
if (!ops) { toast.show(t('timeline.trackLocked')); return; }
|
|
267
|
+
const failed = await edit(ops, `history.stepFroze|${clip.id}`);
|
|
268
|
+
if (failed) toast.showError(failed);
|
|
269
|
+
}, [projectId, edit, editModel, timeRef, toast, t]);
|
|
270
|
+
|
|
271
|
+
/* ── replace ── */
|
|
272
|
+
const replace = React.useCallback(async (block: TimelineBlock, file: ReplaceFile) => {
|
|
273
|
+
const clip: Clip | undefined = filmRef.current?.tracks.flatMap((tr) => tr.clips).find((c) => c.id === block.clipId);
|
|
274
|
+
if (!clip) return;
|
|
275
|
+
if (tracksRef.current.some((tr) => tr.locked && tr.blocks.some((x) => x.id === block.id))) { toast.show(t('timeline.trackLocked')); return; }
|
|
276
|
+
const plan = replaceClip(clip, block.endMs - block.startMs, file);
|
|
277
|
+
if ('error' in plan) { if (plan.error === 'kind') toast.show(t('editorTools.replaceKind')); return; }
|
|
278
|
+
const failed = await edit([{ op: 'props', edits: plan.edits.map((e) => ({ clip: clip.id, ...e })) }], `history.stepReplaced|${clip.id}`);
|
|
279
|
+
if (failed) { toast.showError(failed); return; }
|
|
280
|
+
if (plan.shortMs > 0) toast.show(t('editorTools.replaceShort').replace('{s}', (plan.shortMs / 1000).toFixed(2)));
|
|
281
|
+
}, [edit, toast, t]);
|
|
282
|
+
const onReplaceDrop = React.useCallback((block: TimelineBlock, file: ReplaceFile) => { void replace(block, file); }, [replace]);
|
|
283
|
+
|
|
284
|
+
/* a file from this computer: brought in first, then put in the clip's place once the listing has it with its length
|
|
285
|
+
(probed after it lands); a file whose length never comes is used as it is after a while */
|
|
286
|
+
const pendingReplace = React.useRef<{ block: TimelineBlock; path: string; since: number } | null>(null);
|
|
287
|
+
const [replaceWait, setReplaceWait] = React.useState(0);
|
|
288
|
+
const tryPendingReplace = React.useCallback(() => {
|
|
289
|
+
const wait = pendingReplace.current;
|
|
290
|
+
if (!wait) return;
|
|
291
|
+
const file = media.find((f) => f.path === wait.path);
|
|
292
|
+
const late = Date.now() - wait.since > REPLACE_PROBE_MS;
|
|
293
|
+
if (!file) {
|
|
294
|
+
if (Date.now() - wait.since > REPLACE_GIVE_UP_MS) { pendingReplace.current = null; toast.showError(t('assets.uploadFailed')); }
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const timed = file.kind === 'video' || file.kind === 'audio';
|
|
298
|
+
if (timed && !(file.durationMs && file.durationMs > 0) && !late) return;
|
|
299
|
+
pendingReplace.current = null;
|
|
300
|
+
void replace(wait.block, file);
|
|
301
|
+
}, [media, replace, toast, t]);
|
|
302
|
+
React.useEffect(() => { tryPendingReplace(); }, [tryPendingReplace, replaceWait]);
|
|
303
|
+
React.useEffect(() => {
|
|
304
|
+
if (!replaceWait) return undefined;
|
|
305
|
+
const timer = window.setInterval(() => { if (pendingReplace.current) tryPendingReplace(); else window.clearInterval(timer); }, 500);
|
|
306
|
+
return () => window.clearInterval(timer);
|
|
307
|
+
}, [replaceWait, tryPendingReplace]);
|
|
308
|
+
const replaceFromComputer = React.useCallback(async (block: TimelineBlock) => {
|
|
309
|
+
const clip: Clip | undefined = filmRef.current?.tracks.flatMap((tr) => tr.clips).find((c) => c.id === block.clipId);
|
|
310
|
+
if (!clip || !pickFile) return;
|
|
311
|
+
let path: string | null;
|
|
312
|
+
try { path = await pickFile(familyOfClip(clip)); } catch (e) { toast.showError(e instanceof Error && e.message ? e.message : t('assets.uploadFailed')); return; }
|
|
313
|
+
if (!path) return;
|
|
314
|
+
pendingReplace.current = { block, path, since: Date.now() };
|
|
315
|
+
setReplaceWait((n) => n + 1);
|
|
316
|
+
}, [pickFile, toast, t]);
|
|
317
|
+
|
|
318
|
+
/** What can take a clip's place: the project's files that fit, then one from this computer. */
|
|
319
|
+
const replaceEntries = React.useCallback((block: TimelineBlock): ContextMenuEntry[] => {
|
|
320
|
+
const clip = block.clipId ? filmRef.current?.tracks.flatMap((tr) => tr.clips).find((c) => c.id === block.clipId) : undefined;
|
|
321
|
+
if (!clip) return [];
|
|
322
|
+
const fits = media.filter((f) => canReplace(clip, f));
|
|
323
|
+
return [
|
|
324
|
+
...fits.slice(0, 20).map((f) => ({ id: `replace:${f.path}`, label: f.name, onSelect: () => { void replace(block, f); } })),
|
|
325
|
+
...(pickFile ? [
|
|
326
|
+
...(fits.length ? [{ id: 'replace-sep', separator: true as const }] : []),
|
|
327
|
+
{ id: 'replace-computer', label: t('editorTools.replaceFromComputer'), icon: <HardDriveUpload size={14} />, onSelect: () => { void replaceFromComputer(block); } },
|
|
328
|
+
] : []),
|
|
329
|
+
];
|
|
330
|
+
}, [media, replace, replaceFromComputer, pickFile, t]);
|
|
331
|
+
|
|
332
|
+
/** What the tools add to a clip's menu. */
|
|
333
|
+
const menuExtra = React.useCallback((blocks: readonly TimelineBlock[]): ContextMenuEntry[] => {
|
|
334
|
+
const out: ContextMenuEntry[] = [];
|
|
335
|
+
const one = blocks.length === 1 ? blocks[0]! : null;
|
|
336
|
+
const atMs = timeRef.current;
|
|
337
|
+
const anySound = blocks.some(sounding);
|
|
338
|
+
out.push({
|
|
339
|
+
id: 'normalize',
|
|
340
|
+
label: t('editorTools.normalize'),
|
|
341
|
+
icon: <AudioLines size={14} />,
|
|
342
|
+
disabled: !anySound || measuring,
|
|
343
|
+
hint: t('editorTools.normalizeNone'),
|
|
344
|
+
submenu: LOUDNESS_TARGETS.map((target) => ({
|
|
345
|
+
id: `normalize:${target.id}`,
|
|
346
|
+
label: t(`editorTools.target.${target.id}`).replace('{lufs}', String(target.lufs)),
|
|
347
|
+
onSelect: () => { void normalize(blocks, target.lufs); },
|
|
348
|
+
})),
|
|
349
|
+
});
|
|
350
|
+
if (one?.kind === 'video' && one.clipId && one.src && !/\.(png|jpe?g|gif|webp|avif|svg)$/i.test(one.src)) {
|
|
351
|
+
const inside = freezeSourceMs(one, atMs) != null;
|
|
352
|
+
out.push({ id: 'freeze', label: t('editorTools.freeze'), icon: <Snowflake size={14} />, disabled: !inside, hint: t('editorTools.freezeOutside'), onSelect: () => { void freeze(one); } });
|
|
353
|
+
}
|
|
354
|
+
if (one?.clipId) {
|
|
355
|
+
const choices = replaceEntries(one);
|
|
356
|
+
out.push({
|
|
357
|
+
id: 'replace',
|
|
358
|
+
label: t('editorTools.replace'),
|
|
359
|
+
icon: <Replace size={14} />,
|
|
360
|
+
disabled: !choices.length,
|
|
361
|
+
hint: t('editorTools.replaceNone'),
|
|
362
|
+
submenu: choices,
|
|
363
|
+
});
|
|
364
|
+
const on = atMs >= one.startMs && atMs < one.endMs;
|
|
365
|
+
out.push({ id: 'clip-marker', label: t('editorTools.markerAddClip'), icon: <MarkerGlyph size={14} />, shortcut: keyHint('add-marker'), disabled: !on, hint: t('editorTools.freezeOutside'), onSelect: () => addMarkerHere(one) });
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}, [measuring, media, normalize, freeze, replaceEntries, addMarkerHere, timeRef, t, keyHint]);
|
|
369
|
+
|
|
370
|
+
/* ── what the timeline draws ── */
|
|
371
|
+
const clipMarkersByTrack = React.useMemo(() => {
|
|
372
|
+
const by = new Map<string, PlacedMarker[]>();
|
|
373
|
+
for (const p of placed) {
|
|
374
|
+
if (!p.clip) continue;
|
|
375
|
+
const lane = tracks.find((tr) => tr.blocks.some((b) => b.id === p.clip!.id))?.lane;
|
|
376
|
+
if (lane) by.set(lane, [...(by.get(lane) ?? []), p]);
|
|
377
|
+
}
|
|
378
|
+
return by;
|
|
379
|
+
}, [placed, tracks]);
|
|
380
|
+
const markerLane = React.useMemo(() => ({
|
|
381
|
+
shown: placed.some((p) => !p.clip),
|
|
382
|
+
render: (lane: MarkerLaneView) => <LaneMarkers placed={placed} lane={lane} actions={markerActions} />,
|
|
383
|
+
}), [placed, markerActions]);
|
|
384
|
+
const onAddMarker = React.useCallback(() => addMarkerHere(), [addMarkerHere]);
|
|
385
|
+
/* the ruler's own menu: a marker there (on the film), as M puts one at the playhead */
|
|
386
|
+
const rulerMenu = React.useCallback((atMs: number): ContextMenuEntry[] => [
|
|
387
|
+
{ id: 'add-marker', label: t('editorTools.markerAdd'), icon: <MarkerGlyph size={14} />, shortcut: keyHint('add-marker'), onSelect: () => addMarkerHere(null, atMs) },
|
|
388
|
+
], [addMarkerHere, t, keyHint]);
|
|
389
|
+
const trackOverlay = React.useCallback((track: TimelineTrack, pxPerMs: number) => {
|
|
390
|
+
const here = clipMarkersByTrack.get(track.lane);
|
|
391
|
+
return here ? <ClipMarkers placed={here} pxPerMs={pxPerMs} height={track.height ?? ownTrackHeight(track)} actions={markerActions} /> : null;
|
|
392
|
+
}, [clipMarkersByTrack, markerActions]);
|
|
393
|
+
|
|
394
|
+
/** the mix's level now, for the meters */
|
|
395
|
+
const levels = React.useCallback(() => player.current.levels(), [player]);
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
/** the timeline's tracks with the heights they are drawn at */
|
|
399
|
+
tracks: shownTracks,
|
|
400
|
+
timeline: { solo: soloProp, markerLane, onAddMarker, rulerMenu, trackOverlay, snapPointsMs, onResizeTrack, onReplaceDrop },
|
|
401
|
+
menuExtra,
|
|
402
|
+
/** what can replace a clip (the inspector's Replace) */
|
|
403
|
+
replaceEntries,
|
|
404
|
+
headMenuExtra,
|
|
405
|
+
onKey,
|
|
406
|
+
levels,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pageChangesOf(loc)` for the inspector's "Changes inside the page": the clip's changes from film.html, and which of
|
|
3
|
+
* them the page no longer has an element for, as the film's stage last said (studio/server/stage.js `lostOverrides`).
|
|
4
|
+
*
|
|
5
|
+
* The inspector reads it every second while a page clip's panel is open; the stage is asked only then (a read in the
|
|
6
|
+
* last few seconds), once a second, so a closed panel costs nothing.
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import type { PreviewHandle } from '../editor/Preview';
|
|
10
|
+
import { lostByClip, pageChangesAt, type FilmParts } from '@/lib/page-changes';
|
|
11
|
+
import type { PageChanges } from './FilmInspector';
|
|
12
|
+
|
|
13
|
+
const ASK_EVERY_MS = 1000;
|
|
14
|
+
/** how long after the last read the stage is still asked */
|
|
15
|
+
const KEEP_ASKING_MS = 3000;
|
|
16
|
+
|
|
17
|
+
export function usePageChanges(
|
|
18
|
+
preview: React.RefObject<PreviewHandle | null>,
|
|
19
|
+
film: React.RefObject<FilmParts | null | undefined>,
|
|
20
|
+
): (loc: string) => PageChanges | null {
|
|
21
|
+
const lost = React.useRef<ReadonlyMap<string, readonly string[]>>(new Map());
|
|
22
|
+
const readAt = React.useRef(0);
|
|
23
|
+
const asking = React.useRef(false);
|
|
24
|
+
|
|
25
|
+
const ask = React.useCallback(() => {
|
|
26
|
+
const handle = preview.current;
|
|
27
|
+
if (!handle?.stage || asking.current) return;
|
|
28
|
+
asking.current = true;
|
|
29
|
+
void handle.stage('lostOverrides').then((answer) => {
|
|
30
|
+
/* no answer (a film loading again): keep what was known */
|
|
31
|
+
if (answer != null) lost.current = lostByClip(answer);
|
|
32
|
+
}).finally(() => { asking.current = false; });
|
|
33
|
+
}, [preview]);
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
const id = window.setInterval(() => {
|
|
37
|
+
if (Date.now() - readAt.current < KEEP_ASKING_MS) ask();
|
|
38
|
+
}, ASK_EVERY_MS);
|
|
39
|
+
return () => window.clearInterval(id);
|
|
40
|
+
}, [ask]);
|
|
41
|
+
|
|
42
|
+
return React.useCallback((loc: string) => {
|
|
43
|
+
/* the first read in a while: ask now rather than at the next tick */
|
|
44
|
+
if (Date.now() - readAt.current > KEEP_ASKING_MS) ask();
|
|
45
|
+
readAt.current = Date.now();
|
|
46
|
+
return pageChangesAt(film.current, loc, lost.current);
|
|
47
|
+
}, [ask, film]);
|
|
48
|
+
}
|
package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-project-history.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project's version history and the actions on it, for the Version history panel.
|
|
3
|
+
*
|
|
4
|
+
* const history = useProjectHistory(projectId, { onError: toast.showError, onMoved: reloadFilm });
|
|
5
|
+
* // on the project's `{ type: 'history' }` event: history.refresh()
|
|
6
|
+
* <ProjectHistoryControls history={history} open={open} anchor={anchor} onClose={…} />
|
|
7
|
+
*
|
|
8
|
+
* It loads on mount (and when the project changes), after each of its own actions, and when `refresh()` is called.
|
|
9
|
+
* An action answers with its outcome: refusals the panel asks about (uncommitted changes, a merge that collides, a
|
|
10
|
+
* branch with commits on no other) come back to it; any other failure is said through `onError`.
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { projectEvents } from '@/api';
|
|
14
|
+
import { useT } from '@/i18n';
|
|
15
|
+
import { historyApi, historyErrorKey, historyProgressOf, type HistoryProgress, type HistoryResult, type HistoryState } from '@/lib/project-history';
|
|
16
|
+
|
|
17
|
+
export interface ProjectHistory {
|
|
18
|
+
/** null: not loaded yet. */
|
|
19
|
+
state: HistoryState | null;
|
|
20
|
+
/** An action on its way: the panel waits. */
|
|
21
|
+
busy: string | null;
|
|
22
|
+
/** How far the action on its way is (or the history being loaded, when an older one is brought up to date). */
|
|
23
|
+
progress: HistoryProgress | null;
|
|
24
|
+
refresh: () => Promise<void>;
|
|
25
|
+
commit: (message: string) => Promise<boolean>;
|
|
26
|
+
discard: () => Promise<boolean>;
|
|
27
|
+
restore: (commit: string) => Promise<HistoryResult>;
|
|
28
|
+
checkout: (o: { branch: string; create?: boolean; from?: string; carry?: 'leave' | 'bring' }) => Promise<HistoryResult<{ parkedBack?: boolean }>>;
|
|
29
|
+
rename: (from: string, name: string) => Promise<boolean>;
|
|
30
|
+
remove: (name: string, force?: boolean) => Promise<HistoryResult>;
|
|
31
|
+
merge: (o: { branch: string; message: string; prefer?: 'ours' | 'theirs' }) => Promise<HistoryResult<{ merged?: boolean }>>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Refusals the panel asks the person about instead of saying them. */
|
|
35
|
+
const ASKED = new Set(['dirty', 'conflict', 'unmerged']);
|
|
36
|
+
|
|
37
|
+
export function useProjectHistory(projectId: string, {
|
|
38
|
+
onError,
|
|
39
|
+
onMoved,
|
|
40
|
+
beforeCommit,
|
|
41
|
+
}: {
|
|
42
|
+
/** A failed action, in a sentence for the person (show it as an error toast). */
|
|
43
|
+
onError: (message: string) => void;
|
|
44
|
+
/** The folder now holds other files (gone back, switched, merged, discarded): reload the film. */
|
|
45
|
+
onMoved?: () => void;
|
|
46
|
+
/** Runs before committing: flush edits still waiting to be written, so the commit holds what the person sees. */
|
|
47
|
+
beforeCommit?: () => Promise<unknown>;
|
|
48
|
+
}): ProjectHistory {
|
|
49
|
+
const t = useT();
|
|
50
|
+
const [state, setState] = React.useState<HistoryState | null>(null);
|
|
51
|
+
const [busy, setBusy] = React.useState<string | null>(null);
|
|
52
|
+
const [progress, setProgress] = React.useState<HistoryProgress | null>(null);
|
|
53
|
+
/* a ref as well as state: a second click lands before React re-renders */
|
|
54
|
+
const inFlight = React.useRef(false);
|
|
55
|
+
|
|
56
|
+
const refresh = React.useCallback(async () => {
|
|
57
|
+
const fresh = await historyApi.load(projectId);
|
|
58
|
+
/* not loaded: keep what is shown; one hiccup should not empty the panel */
|
|
59
|
+
if (fresh) setState(fresh);
|
|
60
|
+
/* loading may have brought an older history up to date, with progress of its own */
|
|
61
|
+
if (!inFlight.current) setProgress(null);
|
|
62
|
+
}, [projectId]);
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
setState(null);
|
|
66
|
+
void refresh();
|
|
67
|
+
}, [refresh]);
|
|
68
|
+
|
|
69
|
+
/* how far an action is, as the server says while it runs; the history event at its end clears it */
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
setProgress(null);
|
|
72
|
+
return projectEvents(projectId, (event) => {
|
|
73
|
+
if (event.type === 'history') { setProgress(null); return; }
|
|
74
|
+
const p = historyProgressOf(event);
|
|
75
|
+
if (p) setProgress(p);
|
|
76
|
+
});
|
|
77
|
+
}, [projectId]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run one action at a time. `moves`: it changes the folder's files when it goes through. A refusal the panel asks
|
|
81
|
+
* about comes back to it; any other is said.
|
|
82
|
+
*/
|
|
83
|
+
const act = React.useCallback(async <T,>(name: string, run: () => Promise<HistoryResult<T>>, moves: boolean, asked = ASKED): Promise<HistoryResult<T>> => {
|
|
84
|
+
if (inFlight.current) return { ok: false, status: 429, code: 'busy' };
|
|
85
|
+
inFlight.current = true;
|
|
86
|
+
setBusy(name);
|
|
87
|
+
try {
|
|
88
|
+
const result = await run();
|
|
89
|
+
if (result.ok && moves) onMoved?.();
|
|
90
|
+
if (!result.ok && !(result.code && asked.has(result.code))) onError(t(historyErrorKey(result)));
|
|
91
|
+
await refresh();
|
|
92
|
+
return result;
|
|
93
|
+
} finally {
|
|
94
|
+
inFlight.current = false;
|
|
95
|
+
setBusy(null);
|
|
96
|
+
setProgress(null);
|
|
97
|
+
}
|
|
98
|
+
}, [onMoved, onError, refresh, t]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
state,
|
|
102
|
+
busy,
|
|
103
|
+
progress,
|
|
104
|
+
refresh,
|
|
105
|
+
commit: async (message) => (await act('commit', async () => { await beforeCommit?.(); return historyApi.commit(projectId, message); }, false, new Set())).ok,
|
|
106
|
+
discard: async () => (await act('discard', () => historyApi.discard(projectId), true, new Set())).ok,
|
|
107
|
+
restore: (commit) => act('restore', () => historyApi.restore(projectId, commit), true),
|
|
108
|
+
checkout: (o) => act('checkout', () => historyApi.checkout(projectId, o), true),
|
|
109
|
+
rename: async (from, name) => (await act('rename', () => historyApi.rename(projectId, from, name), false, new Set())).ok,
|
|
110
|
+
remove: (name, force) => act('remove', () => historyApi.remove(projectId, name, force), false),
|
|
111
|
+
merge: (o) => act('merge', () => historyApi.merge(projectId, o), true),
|
|
112
|
+
};
|
|
113
|
+
}
|